UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

187 lines • 10 kB
import type { ChatTransport, UIMessage, UIMessageChunk } from 'ai'; import type { AgentBackend } from './backend.js'; import type { AgentRunFailure } from './run-errors.js'; /** Identity surfaced by the backend's `meta` SSE frame / response headers. */ export interface AgentStreamMeta { runId: string; threadId: string; } /** * How the transport retries a stream that dropped mid-run. It reconnects only to a server that * numbers its frames (SSE `id:`) — without a cursor a reconnect would replay the run into the * message it is already writing. */ export interface ReconnectOptions { /** Attempts before giving up with an error chunk. Default 6. */ maxAttempts?: number; /** Delay before the first attempt, doubled each attempt. Default 500 ms. */ baseDelayMs?: number; /** Cap on the delay between attempts. Default 8000 ms. */ maxDelayMs?: number; } /** Where a live stream stands. `reconnecting` → it dropped and the transport is retrying. */ export type StreamConnectionState = { status: 'live'; } | { status: 'reconnecting'; runId: string; attempt: number; after: number; } /** The run ended while the client was away (the resume answered 404): reload the thread. */ | { status: 'gone'; runId: string; } /** Every attempt failed; the stream ended with an error chunk. */ | { status: 'failed'; runId: string; }; export interface AgentChatTransportOptions { /** * What the transport talks to. Default: an {@link AgentClient} built from `baseUrl`, `headers`, * `getHeaders`, `credentials` and `fetch` below — which are ignored when a backend is given. */ backend?: AgentBackend; /** Retry a dropped stream from its last frame. Default on; `false` turns it off. */ reconnect?: ReconnectOptions | false; /** Fires as a stream drops, reconnects, or gives up. */ onConnectionChange?: (state: StreamConnectionState) => void; /** The server's origin, e.g. `https://api.example.com`. Defaults to `''` (same origin). */ baseUrl?: string; /** The agent's route prefix (`AgentModule`'s `path`, global prefix included). Default `'agent'`. */ path?: string; /** Static headers merged into every request (e.g. a tenant ref). */ headers?: Record<string, string>; /** * Resolved at request time — use for short-lived bearer tokens that * must not be captured once at construction. Merged over `headers`. */ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>; /** Forwarded to `fetch` so cookie-based auth/impersonation works. */ credentials?: RequestCredentials; /** Named agent to run the turn (backend `agent` field). */ agent?: string; /** One of the agent's personas to answer as (backend `persona` field). */ persona?: string; /** * Extra body fields evaluated per send — the hook injects `threadId` * and `pageContext` through this. Returned object is spread into the * request body after the SDK's own `body`. */ getBody?: () => Record<string, unknown>; /** * The run id to resume on mount, if any. Returning `undefined` makes * `reconnectToStream` resolve to `null` WITHOUT hitting the network — * this is the generalized fix for "useChat fires a doomed GET on mount * and surfaces its 404". Wire this to the thread's `activeRunId`. */ getResumeRunId?: () => string | undefined; /** * Fires once `reconnectToStream` has attached to a run's stream, with that run's id. A resumed * stream replays frames but opens with no `meta` frame, so this is the only place a caller learns * which run it is now reading — what a Stop pressed after a reload has to cancel. */ onResumeAttached?: (runId: string) => void; /** Fires whenever a stream emits its `meta` frame. */ onMeta?: (meta: AgentStreamMeta) => void; /** * Fires synchronously at the very start of `sendMessages`/`reconnectToStream`, before any network * call — i.e. strictly before that attempt's own `onMeta` (if any) can fire. Lets a caller reset * "which run is THIS attempt settling" state without racing React's render/effect timing: a * consumer that also read `onMeta` to learn the current run id would otherwise risk misattributing * an early failure (no meta ever arrives) to a PRIOR attempt's id. */ onAttemptStart?: () => void; /** * A resume found nothing streaming under the run id (the backend answered 404): the run already * ended. The SDK is told there is nothing to resume, so the caller is the one that has to reload * what the run wrote — `useAgentChat` re-reads the thread. */ onResumeGone?: (runId: string) => void; /** * The run's stream ended with the backend's `event: error` frame — called with its `code` and * `message` right before the stream is closed with an error chunk. The chunk can only carry the * text; this is where the `code` reaches a caller, so it can word the failure itself. */ onRunError?: (failure: AgentRunFailure) => void; /** Injectable for tests / non-browser runtimes. Defaults to global fetch. */ fetch?: typeof fetch; } /** * AI SDK v7 `ChatTransport` for the Adonis agent backend. POSTs * `/agent/chat`, parses the backend's `meta` + `{delta}` + `done` SSE * frames, and re-emits them as the v7 UI-message chunk stream * (`start` → `text-start` → `text-delta`* → `text-end` → `finish`). * * The backend hydrates prior history from its store, so only the latest * user message text is sent each turn — keeping payloads tiny and * preventing the client from corrupting replayed history. * * Attachments (image/PDF for a vision-capable model) ride the per-send body: * `sendMessage({ text }, { body: { attachments: MessageAttachment[] } })`. They * flow to the backend as the turn's `attachments`, get persisted on the user * message, and are rendered as native model content parts. Optimistic display of * the user's own attachment thumbnails is the consumer's concern (it stages the * upload), the same way history rendering reads `StoredMessage.attachments`. * * Frames beyond text/reasoning/tools become AI SDK data parts (seen by `useChat`'s `onData`): * - `ui` → `data-ui` part, `id` = the component id (a repeat id updates it in place) * - `approval-requested` → `data-approval-requested` part keyed by the call id, plus the SDK's * native `tool-approval-request` (the part moves to `approval-requested`) * - `approval-settled` → `data-approval-settled` part keyed by the call id (who decided, through * what, remembered or not); the outcome still rides the call's own output * - `title`, `cancelled`, `queue` → transient `data-title` / `data-cancelled` / `data-queue` (never * stored on the message). A send the server queued (`202`) is reported * as a transient `data-queue` too, carrying `queuedSend` * - any other kind → `data-<kind>` part carrying the frame minus `kind`, keyed by its `id` * when it has one — forwarded, never dropped * `parentId` on a tool frame rides the part's `toolMetadata` next to `toolKind`. */ export declare class AgentChatTransport implements ChatTransport<UIMessage> { private readonly options; private currentRunId; private currentThreadId; /** * One attempt at a time. The AI SDK keeps a single in-flight response per chat and decides * whether its streamed message REPLACES the list's last entry or is appended by comparing that * response's message id against the last entry alone. Two attempts writing into one chat * therefore append alternating copies of each other's message, and the list ends up holding * several entries under each id — which React renders as duplicate keys. */ private attemptLive; private readonly backend; constructor(options?: AgentChatTransportOptions); /** Run id of the most recent stream — HITL approve/reject target this. */ get runId(): string | undefined; /** Thread id of the most recent stream. */ get threadId(): string | undefined; /** * True from the moment a send or resume attempt starts until its chunk stream terminates. A * caller that can refuse a turn BEFORE the SDK commits to one — `useAgentChat` wrapping * `sendMessage` — reads this; the transport itself can only refuse a resume, which is the one * attempt the SDK lets a transport decline. */ get isAttemptLive(): boolean; sendMessages(options: Parameters<ChatTransport<UIMessage>['sendMessages']>[0]): Promise<ReadableStream<UIMessageChunk>>; reconnectToStream(options: Parameters<ChatTransport<UIMessage>['reconnectToStream']>[0]): Promise<ReadableStream<UIMessageChunk> | null>; private captureHeaderMeta; private reconnectPolicy; private recordMeta; /** * Parse the backend's SSE byte stream and re-emit it as a valid v7 UI-message chunk stream. * Recognized frames: * - `event: meta` `data: {"runId","threadId"}` → records identity * - `data: <AgentStreamEvent JSON>` → mapped to UI chunks (text / reasoning / tool / * `data-*`; see {@link AgentChatTransport} docs) * - `event: done` `data: {}` → terminates * - `event: error` `data: {code,message}` → error chunk * * A run is ONE UI message with N steps. Each `step-start`/`step-finish` pair brackets a model * call plus its tool execution; text and reasoning open lazily and close at the step boundary, so * tool-call cards (input streaming → output) render live between the prose. */ private toChunkStream; } //# sourceMappingURL=agent-chat-transport.d.ts.map