Download src/api/event-service/event-service.api.ts from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 6.72 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/api/event-service/event-service.api.ts
- Command line
-
hf download hf://SaylorTwift/openhands/src/api/event-service/event-service.api.ts
-
curl -L -o event-service.api.ts https://huggingface.co/SaylorTwift/openhands/resolve/main/src/api/event-service/event-service.api.ts
6.72 kB
| import { ConversationClient } from "@openhands/typescript-client/clients"; | |
| import { RemoteEventsList } from "@openhands/typescript-client/events/remote-events-list"; | |
| import { OpenHandsEvent } from "#/types/agent-server/core"; | |
| import { buildHttpBaseUrl } from "#/utils/websocket-url"; | |
| import { getActiveBackend } from "../backend-registry/active-store"; | |
| import { callCloudProxy } from "../cloud/proxy"; | |
| import { | |
| getAgentServerClientOptions, | |
| getAgentServerHttpClientOptions, | |
| } from "../agent-server-client-options"; | |
| import type { | |
| ConfirmationResponseRequest, | |
| ConfirmationResponseResponse, | |
| EventSearchOptions, | |
| EventSearchPage, | |
| } from "./event-service.types"; | |
| /** | |
| * Cloud-mode REST calls are split between two upstream hosts (matching | |
| * OpenHands' cloud frontend): | |
| * | |
| * - **App API** (`backend.host`, default in `callCloudProxy`): | |
| * event *history* (`/api/v1/conversation/{id}/events/search`). | |
| * Persisted by the cloud backend — survives the runtime sandbox. | |
| * | |
| * - **Runtime sandbox** (extracted from `conversation.conversation_url` | |
| * and passed as `hostOverride`): live runtime endpoints like | |
| * `/api/conversations/{id}/events/count` and | |
| * `/api/conversations/{id}/events/respond_to_confirmation`. Auth on | |
| * these endpoints is `X-Session-API-Key`, not `Authorization: Bearer`. | |
| * | |
| * App API calls go directly to the cloud backend with bearer auth. Runtime | |
| * sandbox calls go through `/api/cloud-proxy`, which avoids depending on CORS | |
| * for per-conversation runtime hosts. | |
| * | |
| * Local mode keeps the existing typescript-client path: it targets the | |
| * conversation's host directly via typed client classes. | |
| */ | |
| class EventService { | |
| static async respondToConfirmation( | |
| conversationId: string, | |
| conversationUrl: string, | |
| request: ConfirmationResponseRequest, | |
| sessionApiKey?: string | null, | |
| ): Promise<ConfirmationResponseResponse> { | |
| const active = getActiveBackend().backend; | |
| if (active.kind === "cloud") { | |
| return callCloudProxy<ConfirmationResponseResponse>({ | |
| backend: active, | |
| method: "POST", | |
| hostOverride: buildHttpBaseUrl(conversationUrl), | |
| path: `/api/conversations/${conversationId}/events/respond_to_confirmation`, | |
| body: request, | |
| authMode: "session-api-key", | |
| sessionApiKey, | |
| }); | |
| } | |
| return new ConversationClient( | |
| getAgentServerClientOptions({ | |
| conversationUrl, | |
| sessionApiKey, | |
| }), | |
| ).respondToConfirmation<ConfirmationResponseResponse>( | |
| conversationId, | |
| request, | |
| ); | |
| } | |
| static async getEventCount( | |
| conversationId: string, | |
| conversationUrl: string, | |
| sessionApiKey?: string | null, | |
| ): Promise<number> { | |
| const active = getActiveBackend().backend; | |
| if (active.kind === "cloud") { | |
| return callCloudProxy<number>({ | |
| backend: active, | |
| method: "GET", | |
| hostOverride: buildHttpBaseUrl(conversationUrl), | |
| path: `/api/conversations/${conversationId}/events/count`, | |
| authMode: "session-api-key", | |
| sessionApiKey, | |
| }); | |
| } | |
| return new ConversationClient( | |
| getAgentServerClientOptions({ | |
| conversationUrl, | |
| sessionApiKey, | |
| }), | |
| ).getEventCount(conversationId); | |
| } | |
| /** | |
| * Search events for a conversation. Returns the raw page so callers can | |
| * paginate (via `next_page_id`) and so REST-driven history loading can | |
| * tell when there are no more older events to load. | |
| */ | |
| static async searchEvents( | |
| conversationId: string, | |
| conversationUrl?: string | null, | |
| sessionApiKey?: string | null, | |
| options: EventSearchOptions = {}, | |
| ): Promise<EventSearchPage<OpenHandsEvent>> { | |
| const active = getActiveBackend().backend; | |
| const limit = options.limit ?? 100; | |
| if (active.kind === "cloud") { | |
| // Event *history* lives on the cloud App API, not the runtime | |
| // sandbox. Path is singular `conversation` and v1-prefixed. | |
| // | |
| // Full pagination params (sort_order, page_id, timestamp filters) | |
| // require the server-side fix from OpenHands/OpenHands#14399. If | |
| // the cloud backend hasn't been updated yet, the timestamp filters | |
| // trigger a 500 (str-vs-datetime comparison). We attempt the full | |
| // request first and fall back to a limit-only request on failure. | |
| const cloudLimit = Math.min(limit, 100); | |
| const hasFilterParams = !!( | |
| options.sortOrder || | |
| options.pageId || | |
| options.timestampGte || | |
| options.timestampLt | |
| ); | |
| const params = new URLSearchParams(); | |
| params.set("limit", String(cloudLimit)); | |
| if (options.sortOrder) params.set("sort_order", options.sortOrder); | |
| if (options.pageId) params.set("page_id", options.pageId); | |
| if (options.timestampGte) | |
| params.set("timestamp__gte", options.timestampGte); | |
| if (options.timestampLt) params.set("timestamp__lt", options.timestampLt); | |
| const doCloudSearch = (searchParams: URLSearchParams) => | |
| callCloudProxy<EventSearchPage<OpenHandsEvent>>({ | |
| backend: active, | |
| method: "GET", | |
| path: `/api/v1/conversation/${conversationId}/events/search?${searchParams.toString()}`, | |
| }); | |
| try { | |
| const data = await doCloudSearch(params); | |
| return { | |
| items: data?.items ?? [], | |
| next_page_id: data?.next_page_id ?? null, | |
| }; | |
| } catch (err) { | |
| if (!hasFilterParams) throw err; | |
| if (options.strictPagination) throw err; | |
| // Server doesn't support timestamp filters yet — stop pagination | |
| // by returning an empty page so the UI doesn't retry indefinitely. | |
| // A limit-only fallback would return the same most-recent events | |
| // already in the store, which get deduped but keep hasMore=true. | |
| console.warn( | |
| "[EventService] Cloud backend doesn't support pagination filters. " + | |
| "Falling back to initial load only. " + | |
| "Server needs OpenHands/OpenHands#14399.", | |
| ); | |
| return { items: [], next_page_id: null }; | |
| } | |
| } | |
| const page = await new RemoteEventsList( | |
| getAgentServerHttpClientOptions({ conversationUrl, sessionApiKey }), | |
| conversationId, | |
| ).search({ | |
| limit, | |
| ...(options.pageId ? { page_id: options.pageId } : {}), | |
| ...(options.sortOrder ? { sort_order: options.sortOrder } : {}), | |
| ...(options.timestampGte ? { timestamp__gte: options.timestampGte } : {}), | |
| ...(options.timestampLt ? { timestamp__lt: options.timestampLt } : {}), | |
| }); | |
| return { | |
| items: (page?.items ?? []) as OpenHandsEvent[], | |
| next_page_id: page?.next_page_id ?? null, | |
| }; | |
| } | |
| } | |
| export default EventService; | |