diff options
Diffstat (limited to 'packages/cli/src/http.ts')
| -rw-r--r-- | packages/cli/src/http.ts | 338 |
1 files changed, 169 insertions, 169 deletions
diff --git a/packages/cli/src/http.ts b/packages/cli/src/http.ts index e13842a..8092dfa 100644 --- a/packages/cli/src/http.ts +++ b/packages/cli/src/http.ts @@ -8,222 +8,222 @@ */ import type { - AgentEvent, - ChatRequest, - CompactResponse, - ConversationListResponse, - LastMessageResponse, - ModelsResponse, - OpenConversationResponse, - QueueResponse, + AgentEvent, + ChatRequest, + CompactResponse, + ConversationListResponse, + LastMessageResponse, + ModelsResponse, + OpenConversationResponse, + QueueResponse, } from "@dispatch/transport-contract"; import { splitNdjsonLines } from "./ndjson.js"; interface FetchDeps { - readonly fetchImpl: typeof fetch; + readonly fetchImpl: typeof fetch; } interface StreamChatOpts { - readonly server: string; - readonly request: ChatRequest; + readonly server: string; + readonly request: ChatRequest; } export async function streamChat( - deps: FetchDeps, - opts: StreamChatOpts, + deps: FetchDeps, + opts: StreamChatOpts, ): Promise<{ conversationId: string | null; events: AsyncIterable<AgentEvent> }> { - const url = `${opts.server}/chat`; - const res = await deps.fetchImpl(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(opts.request), - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`POST /chat failed with status ${res.status}: ${body}`); - } - - const conversationId = res.headers.get("X-Conversation-Id"); - - if (!res.body) { - throw new Error("POST /chat returned no body"); - } - - const events = readNdjsonStream(res.body); - return { conversationId, events }; + const url = `${opts.server}/chat`; + const res = await deps.fetchImpl(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(opts.request), + }); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`POST /chat failed with status ${res.status}: ${body}`); + } + + const conversationId = res.headers.get("X-Conversation-Id"); + + if (!res.body) { + throw new Error("POST /chat returned no body"); + } + + const events = readNdjsonStream(res.body); + return { conversationId, events }; } async function* readNdjsonStream(body: ReadableStream<Uint8Array>): AsyncIterable<AgentEvent> { - const reader = body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const { lines, rest } = splitNdjsonLines(buffer); - buffer = rest; - for (const line of lines) { - yield JSON.parse(line) as AgentEvent; - } - } - if (buffer.length > 0) { - yield JSON.parse(buffer) as AgentEvent; - } - } finally { - reader.releaseLock(); - } + const reader = body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const { lines, rest } = splitNdjsonLines(buffer); + buffer = rest; + for (const line of lines) { + yield JSON.parse(line) as AgentEvent; + } + } + if (buffer.length > 0) { + yield JSON.parse(buffer) as AgentEvent; + } + } finally { + reader.releaseLock(); + } } interface FetchModelsOpts { - readonly server: string; + readonly server: string; } export async function fetchModels(deps: FetchDeps, opts: FetchModelsOpts): Promise<ModelsResponse> { - const url = `${opts.server}/models`; - const res = await deps.fetchImpl(url); + const url = `${opts.server}/models`; + const res = await deps.fetchImpl(url); - if (!res.ok) { - const body = await res.text(); - throw new Error(`GET /models failed with status ${res.status}: ${body}`); - } + if (!res.ok) { + const body = await res.text(); + throw new Error(`GET /models failed with status ${res.status}: ${body}`); + } - return (await res.json()) as ModelsResponse; + return (await res.json()) as ModelsResponse; } interface FetchConversationsOpts { - readonly server: string; - readonly query?: string; - readonly status?: string; - readonly workspaceId?: string; + readonly server: string; + readonly query?: string; + readonly status?: string; + readonly workspaceId?: string; } export async function fetchConversations( - deps: FetchDeps, - opts: FetchConversationsOpts, + deps: FetchDeps, + opts: FetchConversationsOpts, ): Promise<ConversationListResponse> { - const params = new URLSearchParams(); - if (opts.query !== undefined) params.set("q", opts.query); - if (opts.status !== undefined) params.set("status", opts.status); - if (opts.workspaceId !== undefined) params.set("workspaceId", opts.workspaceId); - const qs = params.toString(); - const url = qs.length > 0 ? `${opts.server}/conversations?${qs}` : `${opts.server}/conversations`; - const res = await deps.fetchImpl(url); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`GET /conversations failed with status ${res.status}: ${body}`); - } - - return (await res.json()) as ConversationListResponse; + const params = new URLSearchParams(); + if (opts.query !== undefined) params.set("q", opts.query); + if (opts.status !== undefined) params.set("status", opts.status); + if (opts.workspaceId !== undefined) params.set("workspaceId", opts.workspaceId); + const qs = params.toString(); + const url = qs.length > 0 ? `${opts.server}/conversations?${qs}` : `${opts.server}/conversations`; + const res = await deps.fetchImpl(url); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`GET /conversations failed with status ${res.status}: ${body}`); + } + + return (await res.json()) as ConversationListResponse; } interface FetchLastMessageOpts { - readonly server: string; - readonly conversationId: string; + readonly server: string; + readonly conversationId: string; } export async function fetchLastMessage( - deps: FetchDeps, - opts: FetchLastMessageOpts, + deps: FetchDeps, + opts: FetchLastMessageOpts, ): Promise<LastMessageResponse> { - const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/last`; - const res = await deps.fetchImpl(url); + const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/last`; + const res = await deps.fetchImpl(url); - if (!res.ok) { - const body = await res.text(); - throw new Error(`GET /conversations/:id/last failed with status ${res.status}: ${body}`); - } + if (!res.ok) { + const body = await res.text(); + throw new Error(`GET /conversations/:id/last failed with status ${res.status}: ${body}`); + } - return (await res.json()) as LastMessageResponse; + return (await res.json()) as LastMessageResponse; } interface EnqueueMessageOpts { - readonly server: string; - readonly conversationId: string; - readonly text: string; + readonly server: string; + readonly conversationId: string; + readonly text: string; } export async function enqueueMessage( - deps: FetchDeps, - opts: EnqueueMessageOpts, + deps: FetchDeps, + opts: EnqueueMessageOpts, ): Promise<QueueResponse> { - const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/queue`; - const res = await deps.fetchImpl(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ text: opts.text }), - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`POST /conversations/:id/queue failed with status ${res.status}: ${body}`); - } - - return (await res.json()) as QueueResponse; + const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/queue`; + const res = await deps.fetchImpl(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: opts.text }), + }); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`POST /conversations/:id/queue failed with status ${res.status}: ${body}`); + } + + return (await res.json()) as QueueResponse; } interface OpenConversationOpts { - readonly server: string; - readonly conversationId: string; + readonly server: string; + readonly conversationId: string; } export async function openConversation( - deps: FetchDeps, - opts: OpenConversationOpts, + deps: FetchDeps, + opts: OpenConversationOpts, ): Promise<OpenConversationResponse> { - const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/open`; - const res = await deps.fetchImpl(url, { method: "POST" }); + const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/open`; + const res = await deps.fetchImpl(url, { method: "POST" }); - if (!res.ok) { - const body = await res.text(); - throw new Error(`POST /conversations/:id/open failed with status ${res.status}: ${body}`); - } + if (!res.ok) { + const body = await res.text(); + throw new Error(`POST /conversations/:id/open failed with status ${res.status}: ${body}`); + } - return (await res.json()) as OpenConversationResponse; + return (await res.json()) as OpenConversationResponse; } interface CompactConversationOpts { - readonly server: string; - readonly conversationId: string; + readonly server: string; + readonly conversationId: string; } export async function compactConversation( - deps: FetchDeps, - opts: CompactConversationOpts, + deps: FetchDeps, + opts: CompactConversationOpts, ): Promise<CompactResponse> { - const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/compact`; - const res = await deps.fetchImpl(url, { method: "POST" }); + const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/compact`; + const res = await deps.fetchImpl(url, { method: "POST" }); - if (!res.ok) { - const body = await res.text(); - throw new Error(`POST /conversations/:id/compact failed with status ${res.status}: ${body}`); - } + if (!res.ok) { + const body = await res.text(); + throw new Error(`POST /conversations/:id/compact failed with status ${res.status}: ${body}`); + } - return (await res.json()) as CompactResponse; + return (await res.json()) as CompactResponse; } interface StopTurnOpts { - readonly server: string; - readonly conversationId: string; + readonly server: string; + readonly conversationId: string; } export async function stopTurn( - deps: FetchDeps, - opts: StopTurnOpts, + deps: FetchDeps, + opts: StopTurnOpts, ): Promise<{ conversationId: string; abortedTurn: boolean }> { - const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/stop`; - const res = await deps.fetchImpl(url, { method: "POST" }); + const url = `${opts.server}/conversations/${encodeURIComponent(opts.conversationId)}/stop`; + const res = await deps.fetchImpl(url, { method: "POST" }); - if (!res.ok) { - const body = await res.text(); - throw new Error(`POST /conversations/:id/stop failed with status ${res.status}: ${body}`); - } + if (!res.ok) { + const body = await res.text(); + throw new Error(`POST /conversations/:id/stop failed with status ${res.status}: ${body}`); + } - return (await res.json()) as { conversationId: string; abortedTurn: boolean }; + return (await res.json()) as { conversationId: string; abortedTurn: boolean }; } /** @@ -233,8 +233,8 @@ export async function stopTurn( export type ConversationIdResolution = string | { readonly error: string }; interface ResolveConversationIdOpts { - readonly server: string; - readonly shortId: string; + readonly server: string; + readonly shortId: string; } /** @@ -244,28 +244,28 @@ interface ResolveConversationIdOpts { * the candidate short ids + titles so the user can disambiguate. */ export async function resolveConversationId( - deps: FetchDeps, - opts: ResolveConversationIdOpts, + deps: FetchDeps, + opts: ResolveConversationIdOpts, ): Promise<ConversationIdResolution> { - if (opts.shortId.length >= 32) { - return opts.shortId; - } - - const list = await fetchConversations(deps, { server: opts.server, query: opts.shortId }); - const matches = list.conversations; - - if (matches.length === 0) { - return { error: `No conversation matching "${opts.shortId}"` }; - } - - if (matches.length === 1) { - const only = matches[0]; - if (only === undefined) return { error: `No conversation matching "${opts.shortId}"` }; - return only.id; - } - - const lines = matches.map((m) => `${m.id.slice(0, 8)} ${m.title}`).join("\n"); - return { - error: `Multiple conversations matching "${opts.shortId}"\n${lines}`, - }; + if (opts.shortId.length >= 32) { + return opts.shortId; + } + + const list = await fetchConversations(deps, { server: opts.server, query: opts.shortId }); + const matches = list.conversations; + + if (matches.length === 0) { + return { error: `No conversation matching "${opts.shortId}"` }; + } + + if (matches.length === 1) { + const only = matches[0]; + if (only === undefined) return { error: `No conversation matching "${opts.shortId}"` }; + return only.id; + } + + const lines = matches.map((m) => `${m.id.slice(0, 8)} ${m.title}`).join("\n"); + return { + error: `Multiple conversations matching "${opts.shortId}"\n${lines}`, + }; } |
