| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- import type {
- SessionsListInput,
- SessionsListOutput,
- SessionsCreateInput,
- SessionsCreateOutput,
- SessionsActiveOutput,
- SessionsGetInput,
- SessionsGetOutput,
- SessionsSwitchAgentInput,
- SessionsSwitchAgentOutput,
- SessionsSwitchModelInput,
- SessionsSwitchModelOutput,
- SessionsPromptInput,
- SessionsPromptOutput,
- SessionsCompactInput,
- SessionsCompactOutput,
- SessionsWaitInput,
- SessionsWaitOutput,
- SessionsStageInput,
- SessionsStageOutput,
- SessionsClearInput,
- SessionsClearOutput,
- SessionsCommitInput,
- SessionsCommitOutput,
- SessionsContextInput,
- SessionsContextOutput,
- SessionsEventsInput,
- SessionsEventsOutput,
- SessionsInterruptInput,
- SessionsInterruptOutput,
- SessionsMessageInput,
- SessionsMessageOutput,
- } from "./types"
- import { ClientError } from "./client-error"
- export interface ClientOptions {
- readonly baseUrl: string
- readonly fetch?: typeof globalThis.fetch
- readonly headers?: HeadersInit
- }
- export interface RequestOptions {
- readonly signal?: AbortSignal
- readonly headers?: HeadersInit
- }
- interface RequestDescriptor {
- readonly method: string
- readonly path: string
- readonly query?: Record<string, unknown>
- readonly headers?: Record<string, unknown>
- readonly body?: unknown
- readonly successStatus: number
- readonly declaredStatuses: ReadonlyArray<number>
- readonly empty: boolean
- }
- export function make(options: ClientOptions) {
- const fetch = options.fetch ?? globalThis.fetch
- const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {
- const url = new URL(descriptor.path, options.baseUrl)
- for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value)
- const headers = new Headers(options.headers)
- for (const [key, value] of Object.entries(descriptor.headers ?? {})) {
- if (value !== undefined && value !== null) headers.set(key, String(value))
- }
- for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value)
- if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json")
- return {
- url,
- init: {
- method: descriptor.method,
- signal: requestOptions?.signal,
- headers,
- body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body),
- } satisfies RequestInit,
- }
- }
- const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {
- try {
- const prepared = prepare(descriptor, requestOptions)
- return await fetch(prepared.url, prepared.init)
- } catch (cause) {
- throw new ClientError("Transport", { cause })
- }
- }
- const responseError = async (response: Response, descriptor: RequestDescriptor): Promise<never> => {
- if (descriptor.declaredStatuses.includes(response.status)) throw await json(response)
- try {
- await response.body?.cancel()
- } catch {}
- throw new ClientError("UnexpectedStatus", { cause: { status: response.status } })
- }
- const request = async <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise<A> => {
- const response = await execute(descriptor, requestOptions)
- if (response.status !== descriptor.successStatus) return responseError(response, descriptor)
- if (descriptor.empty) {
- try {
- await response.body?.cancel()
- } catch {}
- return undefined as A
- }
- return (await json(response)) as A
- }
- const sse = <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable<A> => ({
- async *[Symbol.asyncIterator]() {
- const response = await execute(descriptor, requestOptions)
- if (response.status !== descriptor.successStatus) await responseError(response, descriptor)
- if (!isContentType(response, "text/event-stream")) {
- try {
- await response.body?.cancel()
- } catch {}
- throw new ClientError("UnsupportedContentType")
- }
- if (response.body === null) throw new ClientError("MalformedResponse")
- const reader = response.body.getReader()
- const decoder = new TextDecoder()
- let buffer = ""
- try {
- while (true) {
- let next
- try {
- next = await reader.read()
- } catch (cause) {
- throw new ClientError("Transport", { cause })
- }
- buffer += decoder.decode(next.value, { stream: !next.done })
- if (buffer.length > 1_048_576) throw new ClientError("MalformedResponse")
- const trailingCarriageReturn = !next.done && buffer.endsWith("\r")
- if (trailingCarriageReturn) buffer = buffer.slice(0, -1)
- buffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n")
- if (trailingCarriageReturn) buffer += "\r"
- if (next.done && buffer !== "") buffer += "\n\n"
- let boundary = buffer.indexOf("\n\n")
- while (boundary >= 0) {
- const block = buffer.slice(0, boundary)
- buffer = buffer.slice(boundary + 2)
- const data = block
- .split("\n")
- .flatMap((line) => (line.startsWith("data:") ? [line.slice(5).trimStart()] : []))
- .join("\n")
- if (data !== "") {
- try {
- yield JSON.parse(data) as A
- } catch (cause) {
- throw new ClientError("MalformedResponse", { cause })
- }
- }
- boundary = buffer.indexOf("\n\n")
- }
- if (next.done) return
- }
- } finally {
- try {
- await reader.cancel()
- } catch {}
- reader.releaseLock()
- }
- },
- })
- return {
- sessions: {
- list: (input?: SessionsListInput, requestOptions?: RequestOptions) =>
- request<SessionsListOutput>(
- {
- method: "GET",
- path: `/api/session`,
- query: {
- workspace: input?.workspace,
- limit: input?.limit,
- order: input?.order,
- search: input?.search,
- directory: input?.directory,
- project: input?.project,
- subpath: input?.subpath,
- cursor: input?.cursor,
- },
- successStatus: 200,
- declaredStatuses: [400, 401],
- empty: false,
- },
- requestOptions,
- ),
- create: (input?: SessionsCreateInput, requestOptions?: RequestOptions) =>
- request<{ readonly data: SessionsCreateOutput }>(
- {
- method: "POST",
- path: `/api/session`,
- body: { id: input?.id, agent: input?.agent, model: input?.model, location: input?.location },
- successStatus: 200,
- declaredStatuses: [401, 400],
- empty: false,
- },
- requestOptions,
- ).then((value) => value.data),
- active: (requestOptions?: RequestOptions) =>
- request<{ readonly data: SessionsActiveOutput }>(
- {
- method: "GET",
- path: `/api/session/active`,
- successStatus: 200,
- declaredStatuses: [401, 400],
- empty: false,
- },
- requestOptions,
- ).then((value) => value.data),
- get: (input: SessionsGetInput, requestOptions?: RequestOptions) =>
- request<{ readonly data: SessionsGetOutput }>(
- {
- method: "GET",
- path: `/api/session/${encodeURIComponent(input.sessionID)}`,
- successStatus: 200,
- declaredStatuses: [404, 400, 401],
- empty: false,
- },
- requestOptions,
- ).then((value) => value.data),
- switchAgent: (input: SessionsSwitchAgentInput, requestOptions?: RequestOptions) =>
- request<SessionsSwitchAgentOutput>(
- {
- method: "POST",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`,
- body: { agent: input.agent },
- successStatus: 204,
- declaredStatuses: [404, 400, 401],
- empty: true,
- },
- requestOptions,
- ),
- switchModel: (input: SessionsSwitchModelInput, requestOptions?: RequestOptions) =>
- request<SessionsSwitchModelOutput>(
- {
- method: "POST",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/model`,
- body: { model: input.model },
- successStatus: 204,
- declaredStatuses: [404, 400, 401],
- empty: true,
- },
- requestOptions,
- ),
- prompt: (input: SessionsPromptInput, requestOptions?: RequestOptions) =>
- request<{ readonly data: SessionsPromptOutput }>(
- {
- method: "POST",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`,
- body: { id: input.id, prompt: input.prompt, delivery: input.delivery, resume: input.resume },
- successStatus: 200,
- declaredStatuses: [409, 404, 400, 401],
- empty: false,
- },
- requestOptions,
- ).then((value) => value.data),
- compact: (input: SessionsCompactInput, requestOptions?: RequestOptions) =>
- request<SessionsCompactOutput>(
- {
- method: "POST",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
- successStatus: 204,
- declaredStatuses: [404, 503, 400, 401],
- empty: true,
- },
- requestOptions,
- ),
- wait: (input: SessionsWaitInput, requestOptions?: RequestOptions) =>
- request<SessionsWaitOutput>(
- {
- method: "POST",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`,
- successStatus: 204,
- declaredStatuses: [404, 503, 400, 401],
- empty: true,
- },
- requestOptions,
- ),
- stage: (input: SessionsStageInput, requestOptions?: RequestOptions) =>
- request<{ readonly data: SessionsStageOutput }>(
- {
- method: "POST",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
- body: { messageID: input.messageID, files: input.files },
- successStatus: 200,
- declaredStatuses: [404, 500, 400, 401],
- empty: false,
- },
- requestOptions,
- ).then((value) => value.data),
- clear: (input: SessionsClearInput, requestOptions?: RequestOptions) =>
- request<SessionsClearOutput>(
- {
- method: "POST",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
- successStatus: 204,
- declaredStatuses: [404, 500, 400, 401],
- empty: true,
- },
- requestOptions,
- ),
- commit: (input: SessionsCommitInput, requestOptions?: RequestOptions) =>
- request<SessionsCommitOutput>(
- {
- method: "POST",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
- successStatus: 204,
- declaredStatuses: [404, 400, 401],
- empty: true,
- },
- requestOptions,
- ),
- context: (input: SessionsContextInput, requestOptions?: RequestOptions) =>
- request<{ readonly data: SessionsContextOutput }>(
- {
- method: "GET",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/context`,
- successStatus: 200,
- declaredStatuses: [404, 500, 400, 401],
- empty: false,
- },
- requestOptions,
- ).then((value) => value.data),
- events: (input: SessionsEventsInput, requestOptions?: RequestOptions): AsyncIterable<SessionsEventsOutput> =>
- sse<SessionsEventsOutput>(
- {
- method: "GET",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/event`,
- query: { after: input.after },
- successStatus: 200,
- declaredStatuses: [404, 400, 401],
- empty: false,
- },
- requestOptions,
- ),
- interrupt: (input: SessionsInterruptInput, requestOptions?: RequestOptions) =>
- request<SessionsInterruptOutput>(
- {
- method: "POST",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`,
- successStatus: 204,
- declaredStatuses: [404, 400, 401],
- empty: true,
- },
- requestOptions,
- ),
- message: (input: SessionsMessageInput, requestOptions?: RequestOptions) =>
- request<{ readonly data: SessionsMessageOutput }>(
- {
- method: "GET",
- path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`,
- successStatus: 200,
- declaredStatuses: [404, 400, 401],
- empty: false,
- },
- requestOptions,
- ).then((value) => value.data),
- },
- }
- }
- function appendQuery(params: URLSearchParams, key: string, value: unknown): void {
- if (value === undefined || value === null) return
- if (Array.isArray(value)) {
- for (const item of value) appendQuery(params, key, item)
- return
- }
- if (typeof value === "object") {
- for (const [child, item] of Object.entries(value)) appendQuery(params, `${key}[${child}]`, item)
- return
- }
- params.append(key, String(value))
- }
- async function json(response: Response): Promise<unknown> {
- if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) {
- try {
- await response.body?.cancel()
- } catch {}
- throw new ClientError("UnsupportedContentType")
- }
- let text: string
- try {
- text = await response.text()
- } catch (cause) {
- throw new ClientError("Transport", { cause })
- }
- if (text === "") throw new ClientError("MalformedResponse")
- try {
- return JSON.parse(text)
- } catch (cause) {
- throw new ClientError("MalformedResponse", { cause })
- }
- }
- function isContentType(response: Response, expected: string) {
- return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected
- }
|