/** * SmoothAgentClient — a minimal, idiomatic, transport-agnostic client for the * smooth-operator WebSocket protocol. * * Design goals * ------------ * - **Transport-agnostic.** The client never touches a real socket directly; it * talks to an injectable {@link Transport}. The default ({@link WebSocketTransport}) * uses the global `WebSocket`, but tests inject a mock and Node can inject `ws`. * - **Request/response correlation by `requestId`.** Every action gets a generated * `requestId`; the client routes incoming events back to the originating call. * - **Streaming as an async iterator.** `sendMessage` returns a {@link MessageTurn} * that is both awaitable (resolves with the terminal `eventual_response`) and * async-iterable (yields each `stream_token` / `stream_chunk` / HITL event in * order). This models the `stream_token`/`stream_chunk` → `eventual_response` * flow without forcing a callback style on the caller. * - **No live server required.** Correctness is fully unit-testable with a mock * transport (see `test/client.test.ts`). */ import { type Transport, type WebSocketFactory } from './transport.js'; import type { Cancelled, CreateConversationSessionRequest, CreateConversationSessionResponse, EventualResponse, GetMessagesRequest, GetMessagesResponse, GetSessionRequest, GetSessionResponse, SendMessageRequest, ServerEvent } from './types.js'; export interface SmoothAgentClientOptions { /** WebSocket URL, e.g. `wss://realtime.prod.smooth-agent.dev`. */ url: string; /** * Optional connection auth token for token-gated servers (e.g. the local-flavor * server). When set, the token is appended to the connection URL as a `?token=` * query parameter — browsers can't set custom headers on a WebSocket handshake, * so the token rides the query string, which is where the server reads it from. * Any existing query string on `url` is preserved. This applies to the default * transport only; if a custom {@link transport} is injected, supply the token to * that transport yourself. */ token?: string; /** Inject a transport (for tests / non-browser runtimes). Defaults to a WebSocket transport. */ transport?: Transport; /** Inject a WebSocket factory used by the default transport (e.g. the `ws` package on Node). */ webSocketFactory?: WebSocketFactory; /** Generate request IDs. Defaults to `crypto.randomUUID()` with a `req-` prefix. */ generateRequestId?: () => string; /** Per-request timeout in ms for non-streaming actions. Default 30000. */ requestTimeout?: number; /** * Overall timeout in ms for a streaming `sendMessage` turn: if the server accepts * the message but never emits a terminal `eventual_response` / `error`, the turn * rejects with a {@link TurnTimeoutError} instead of hanging forever. Default * 120000. Set to 0 (or a negative number) to disable. */ turnTimeout?: number; } /** One row returned by {@link SmoothAgentClient.listConversations} — enough to * render a sidebar entry and resume the conversation on click. */ export interface ConversationSummary { /** Pass to {@link SmoothAgentClient.createConversationSession} as `conversationId` to resume. */ conversationId: string; /** Short preview title derived from the first message (may be empty). */ title: string; /** ISO-8601 last-activity timestamp (rows come most-recent first). */ updatedAt: string; /** Number of messages in the conversation. */ messageCount: number; } /** Payload of the `list_conversations` `immediate_response`. */ export interface ListConversationsResponse { conversations: ConversationSummary[]; } /** * A streaming turn that received no terminal `eventual_response` / `error` within the * configured {@link SmoothAgentClientOptions.turnTimeout}. The turn rejects with this * and its async iteration throws it, so a stuck server can never hang the caller. */ export declare class TurnTimeoutError extends Error { readonly requestId: string; constructor(requestId: string, ms: number); } /** A protocol-level error event surfaced as a throwable. */ export declare class ProtocolError extends Error { readonly code: string; readonly requestId?: string; constructor(code: string, message: string, requestId?: string); } /** * A streaming message turn. Await it for the terminal {@link EventualResponse}, * or async-iterate it to receive every intermediate event in arrival order. * * ```ts * const turn = client.sendMessage({ sessionId, message: 'hi' }); * for await (const ev of turn) { * if (ev.type === 'stream_token') process.stdout.write(ev.token ?? ''); * } * const final = await turn; // EventualResponse * ``` */ export declare class MessageTurn implements AsyncIterable, PromiseLike { /** The requestId this turn is correlated on. */ readonly requestId: string; private readonly queue; private waiter; private done; private finalEvent; private error; private _cancelled; private readonly settled; private settle; private fail; private readonly onClose; private readonly onCancel; private timeoutTimer; constructor(requestId: string, onClose: () => void, turnTimeout?: number, onCancel?: () => void); /** * True if this turn ended because the user stopped it — a terminal `cancelled` * event settled it, as opposed to completing (`eventual_response`) or erroring. * This is the UI's signal to distinguish a user-stop from a failure: on a * user-stop the turn *resolves* (never rejects), and `await turn` yields the * terminal `Cancelled` event. */ get cancelled(): boolean; /** Feed an event into the turn (called by the client's dispatcher). */ push(event: ServerEvent): void; /** * Request cancellation of THIS turn — the ergonomic "stop this turn" button. * Sends a `cancel` frame carrying the turn's own `requestId` (and the originating * `sessionId`) via the client. Idempotent: a no-op once the turn has already * settled. The turn itself settles when the server's terminal `cancelled` event * arrives (see {@link cancelled}); this method only sends the request. */ cancel(): void; /** Force-close the turn (e.g. on disconnect) with an error. */ abort(err: unknown): void; private deliver; private finish; [Symbol.asyncIterator](): AsyncIterator; then(onfulfilled?: ((value: EventualResponse | Cancelled) => TResult1 | PromiseLike) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null): PromiseLike; } export declare class SmoothAgentClient { private readonly transport; private readonly generateRequestId; private readonly requestTimeout; private readonly turnTimeout; /** requestId → single-response waiter (create_session, get_session, ping, …). */ private readonly pending; /** requestId → active streaming turn (send_message, and HITL resumes). */ private readonly turns; /** Unsolicited-event listeners (keepalive, server-push). */ private readonly listeners; private unsubscribe; constructor(options: SmoothAgentClientOptions); /** Open the underlying transport. */ connect(): Promise; /** Close the transport and reject all in-flight work. */ disconnect(reason?: string): void; /** Subscribe to unsolicited / uncorrelated server events (e.g. keepalive). */ onEvent(listener: (event: ServerEvent) => void): () => void; /** * Start a new conversation session — or **resume** an existing one by passing * its `conversationId`. On resume the server binds the new session to that * conversation (reusing its id + org + persisted history) so subsequent * `send_message`s append to it; pair it with {@link getMessages} to load the * transcript. Resolves with the session descriptor. */ createConversationSession(req: Omit & { conversationId?: string; }): Promise; /** * List the org's conversations that have at least one message, most-recent * first — the substrate for a conversation sidebar / resume picker. Each row * carries a short title preview, `updatedAt`, and a message count. Pass * `limit` to cap the result (server default 50). * * `list_conversations` has no dedicated action schema in `spec/` yet, so it is * not a member of the generated `ClientAction` union — hence the local cast. * ponytail: promote to a real spec/actions schema if a second consumer needs * it typed end-to-end. */ listConversations(req?: { limit?: number; }): Promise; /** Fetch a session snapshot by ID. */ getSession(req: Omit): Promise; /** Fetch a page of conversation messages. */ getMessages(req: Omit): Promise; /** Keepalive ping. Resolves with the server timestamp from the `pong` event. */ ping(): Promise; /** * Submit a user message and return a {@link MessageTurn}: await it for the * terminal `eventual_response`, or async-iterate it for the streaming events. */ sendMessage(req: Omit): MessageTurn; /** * Client-initiated turn cancellation — the "Stop" button. Sends a `cancel` frame * for the in-flight `send_message` turn identified by `requestId`. The server aborts * the turn's LLM + tool work, frees the turn slot, and emits a terminal `cancelled` * event (in place of `eventual_response`) echoing that `requestId`; the matching * {@link MessageTurn} then settles as a user-stop — it *resolves* (never rejects), * `await turn` yields the `Cancelled` event, and `turn.cancelled` is `true`. * * Idempotent: a cancel with no active turn is a silent server no-op, and calling * this never throws on that account. For the common "stop THIS turn" case, prefer * {@link MessageTurn.cancel}. */ cancel(req: { requestId: string; sessionId?: string; }): void; /** * Approve or reject a pending tool write, resuming the paused turn identified * by `requestId`. The resumed streaming events flow back into the original * {@link MessageTurn} for that `requestId`. */ confirmToolAction(req: { sessionId: string; requestId: string; approved: boolean; }): void; /** * Submit an OTP code, resuming the paused turn identified by `requestId`. * The resumed streaming events flow back into the original {@link MessageTurn}. */ verifyOtp(req: { sessionId: string; requestId: string; code: string; }): void; /** * Submit (or decline) a Rich Interaction, resuming the turn parked by an * `interaction_required` event. This ONE verb serves every interaction kind * (identity intake, future date pickers, choice chips, …) — adding a kind * needs no new client method. Server-side validation may reply with an * `interaction_invalid` event (the turn stays parked — resubmit); a valid * submit resumes the stream back into the original {@link MessageTurn}. */ submitInteraction(req: { sessionId: string; requestId: string; /** Echo of the `interaction_required` event's `interactionId`. */ interactionId: string; /** Optional kind cross-check (e.g. `identity_intake`). */ kind?: string; /** Kind-shaped values (identity_intake: `{ name?, email?, phone? }`). */ values?: Record; declined?: boolean; }): void; /** Send an action that expects a single correlated response event. */ private request; /** Parse and route an incoming frame to the right consumer. */ private handleFrame; private failAll; } //# sourceMappingURL=client.d.ts.map