File size: 4,347 Bytes
3d700dd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | import { BashClient } from "@openhands/typescript-client/clients";
import type {
BashEvent,
BashEventPage,
BashOutput,
} from "@openhands/typescript-client";
import { buildHttpBaseUrl } from "#/utils/websocket-url";
import { getActiveBackend } from "../backend-registry/active-store";
import { callCloudProxy } from "../cloud/proxy";
import { getAgentServerClientOptions } from "../agent-server-client-options";
interface SearchOptions {
kind__eq?: "BashCommand" | "BashOutput";
command_id__eq?: string;
sort_order?: "TIMESTAMP" | "TIMESTAMP_DESC";
page_id?: string;
limit?: number;
}
const MAX_OUTPUT_PAGES = 20; // safety cap; >2000 output events is unlikely.
function isBashOutput(event: BashEvent): event is BashOutput {
return event.kind === "BashOutput";
}
/**
* Cloud-aware bash event reads.
*
* Bash events live on the agent-server runtime that owns the
* conversation. In **local** mode we talk to the active backend's
* agent-server directly with the SDK's `BashClient` (a per-conversation
* URL is honoured when known, otherwise we fall back to the backend
* host — a single local agent-server hosts all conversations). In
* **cloud** mode we tunnel through `callCloudProxy` with the runtime URL
* as `hostOverride`: direct browser calls to `*.prod-runtime.all-hands.dev`
* are blocked by CORS, and runtime endpoints authenticate with the
* conversation's `X-Session-API-Key`.
*
* Note on the search filter name: the agent-server API uses
* `command_id__eq` (not `bash_command_id__eq`) — that's the parameter the
* `BashService.search_bash_events` Python implementation declares and
* what the typescript-client's `BashEventSearchOptions` exposes.
*/
class BashService {
/**
* Fetch all `BashOutput` events for a bash command, paginated and
* sorted by timestamp. Returns events in command-emission order so
* callers can concatenate `stdout` / `stderr` values directly.
*/
static async listOutputs(
conversationUrl: string | null,
sessionApiKey: string | null | undefined,
bashCommandId: string,
): Promise<BashOutput[]> {
const outputs: BashOutput[] = [];
let pageId: string | undefined;
for (let i = 0; i < MAX_OUTPUT_PAGES; i += 1) {
const page = await BashService.searchEvents(
conversationUrl,
sessionApiKey,
{
kind__eq: "BashOutput",
command_id__eq: bashCommandId,
sort_order: "TIMESTAMP",
...(pageId ? { page_id: pageId } : {}),
},
);
page.items.forEach((event) => {
if (isBashOutput(event)) outputs.push(event);
});
if (!page.next_page_id) break;
pageId = page.next_page_id;
}
return outputs;
}
private static async searchEvents(
conversationUrl: string | null,
sessionApiKey: string | null | undefined,
options: SearchOptions,
): Promise<BashEventPage> {
const active = getActiveBackend().backend;
if (active.kind === "cloud") {
// Cloud requires the per-conversation runtime URL — there is no
// shared cloud host that owns bash events. Callers must wait for
// the conversation to be hydrated before invoking this method on
// a cloud backend.
if (!conversationUrl) {
throw new Error(
"BashService.listOutputs requires a conversation URL on cloud backends",
);
}
const params = new URLSearchParams();
Object.entries(options).forEach(([k, v]) => {
if (v !== undefined && v !== null) params.set(k, String(v));
});
return callCloudProxy<BashEventPage>({
backend: active,
method: "GET",
hostOverride: buildHttpBaseUrl(conversationUrl),
path: `/api/bash/bash_events/search?${params.toString()}`,
authMode: "session-api-key",
sessionApiKey,
});
}
// Local mode: the active backend's agent-server hosts the bash
// events. The optional `conversationUrl` is used when present (lets
// us target a per-conversation sub-host), otherwise we fall through
// to `backend.host` via `getAgentServerClientOptions`.
return new BashClient(
getAgentServerClientOptions({
...(conversationUrl ? { conversationUrl } : {}),
sessionApiKey,
}),
).searchEvents(options);
}
}
export default BashService;
|