@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
163 lines • 7.53 kB
TypeScript
import type { ActionProposal, ActionProposalMutationResult } from '../spi/action-proposal-store.js';
import type { UiCapabilities } from '../ui-capabilities.js';
import { type ChatFrame, type ChatPart } from './sse.js';
/**
* Body of `POST /agent/chat` (shape accepted by the provider → `AgentService.chat`). `message` is
* required; everything else is optional context. `pageContext` reaches the tools' ctx (not the model
* prompt) and is rebuilt from each turn's input, so send it on every message, not just the first.
*/
export interface AgentChatRequestBody {
message: string;
threadId?: string;
agent?: string;
persona?: string;
pageContext?: {
kind?: string;
[key: string]: unknown;
};
uiCapabilities?: UiCapabilities;
/** Uploads named by id alone (`POST <base>/attachments` answered them). */
attachments?: unknown[];
/** Run THIS turn on a catalog model (`GET <base>/models`). Never pins it on the thread. */
model?: string;
/**
* Answer the thread's last user message again (needs `threadId`; `message` is ignored): the
* server drops the old answer and streams a new one under a new run.
*/
regenerate?: true;
/**
* What to do when the thread already has a turn running: `'auto'` (default — run now, else wait
* in the thread's queue), `'queue'` (always queue), `'interrupt'` (cancel the running turn and run
* this next). A queued send answers `202` — see {@link AgentChatQueued}.
*/
mode?: 'auto' | 'queue' | 'interrupt';
}
/**
* What `POST {basePath}/chat` answers (`202`) when the message waits in its thread's queue instead
* of running: nothing streams. `runId` is present when it started straight away (attach with
* {@link AgentChatClient.resume}); otherwise it runs — under `messageId` as its run id — once the
* turn ahead of it settles, announced by a `queue` event with `started` in that turn's stream.
*/
export interface AgentChatQueued {
queued: true;
threadId: string;
messageId: string;
/** 0-based place in the queue (`0` → runs next). */
position: number;
queue: {
items: {
id: string;
content: string;
[key: string]: unknown;
}[];
paused: unknown;
};
runId?: string;
/** The run an interrupt cancelled to make room for it. */
interrupting?: string;
}
/** Options for {@link createAgentChatClient}. */
export interface AgentChatClientOptions {
/**
* Base path the provider mounted its routes under (`POST {basePath}/chat`,
* `GET {basePath}/chat/:runId/stream`). Defaults to `/agent`.
*/
basePath?: string;
/** `fetch` implementation to use. Defaults to `globalThis.fetch`. */
fetch?: typeof fetch;
/**
* Extra headers merged into every request — e.g. an anti-CSRF token (`{ 'X-XSRF-TOKEN': ... }`).
* Called per request so a rotating token is always read fresh.
*/
getHeaders?: () => Record<string, string>;
/** Reconnection policy for a dropped stream, or `false` to disable and fail on the first drop. */
resume?: AgentChatResumeOptions | false;
}
/** Reconnection policy: how many times to re-attach, and the backoff before each attempt. */
export interface AgentChatResumeOptions {
/** Max consecutive re-attach attempts before giving up. Defaults to 6. */
maxAttempts?: number;
/** Backoff before attempt `n` (1-based), in ms. Defaults to `min(400 * n, 3000)`. */
backoffMs?: (attempt: number) => number;
}
/** Callbacks fired while a stream is consumed. All optional. */
export interface AgentChatHandlers {
/** The assistant message's parts so far, rebuilt on every frame (safe to render directly). */
onParts?: (parts: ChatPart[]) => void;
/** Each decoded renderable/control frame, in wire order. */
onFrame?: (frame: ChatFrame) => void;
/** The run id, as soon as it is known (response header, then the `meta` frame). */
onRunId?: (runId: string) => void;
/** The thread id, as soon as it is known. */
onThreadId?: (threadId: string) => void;
}
/** Arguments to {@link AgentChatClient.send}. */
export interface AgentChatSendOptions extends AgentChatHandlers {
body: AgentChatRequestBody;
signal?: AbortSignal;
}
/** The settled result of a completed chat turn. */
export interface AgentChatResult {
runId?: string;
threadId?: string;
parts: ChatPart[];
/** Present when the server queued the message instead of streaming a reply (`202`). */
queued?: AgentChatQueued;
proposalDecision?: ActionProposalMutationResult | {
status: 'ambiguous';
proposalIds: string[];
};
}
/**
* Thrown when a run's stream dropped and could not be resumed within the retry budget. Carries the
* partial parts received so far (and the ids) so a consumer can keep what streamed and surface a
* "couldn't finish, try again" state instead of losing the message.
*/
export declare class AgentChatDisconnectedError extends Error {
readonly runId: string | undefined;
readonly threadId: string | undefined;
readonly parts: ChatPart[];
constructor(runId: string | undefined, threadId: string | undefined, parts: ChatPart[]);
}
/**
* The server refused the request before any stream started — a non-2xx answer. `message` is the
* server's own words (`{ message, code? }`, the body every refusal answers), `code` its
* machine-readable reason (`quota_exceeded`, `model_not_allowed`, `thread_required`, …) and `body`
* the parsed answer, or `undefined` when it was not JSON.
*/
export declare class AgentChatHttpError extends Error {
readonly status: number;
readonly code: string | undefined;
readonly body: unknown;
constructor(status: number, message: string, code: string | undefined, body: unknown);
}
/**
* The server ended the run with `event: error` (agent protocol). Not a dropped connection: the run
* is over, so it is surfaced rather than re-attached. `code` is the server's (`quota_exceeded`,
* `output_rejected`, `structured_output_invalid`, `run_failed`).
*/
export declare class AgentChatStreamError extends Error {
readonly code: string;
constructor(code: string, message: string);
}
/**
* A framework-agnostic client for the agent chat SSE endpoints. `send` posts a turn and streams the
* reply; if the connection drops before the run finishes, it re-attaches to
* `GET {basePath}/chat/:runId/stream` — which replays the whole stream from the start and follows live
* — with backoff, until the run emits `done` or the retry budget is exhausted. The run itself is
* durable and keeps executing server-side across the drop, so no tokens are lost.
*/
export interface AgentChatClient {
send(options: AgentChatSendOptions): Promise<AgentChatResult>;
listActionProposals(threadId: string, signal?: AbortSignal): Promise<ActionProposal[]>;
decideActionProposal(threadId: string, proposalId: string, decision: 'approved' | 'rejected', command?: {
remember?: boolean;
reason?: string;
}, signal?: AbortSignal): Promise<ActionProposalMutationResult>;
/** Re-attach to an already-started run (e.g. after a fresh page load) and stream it to completion. */
resume(runId: string, options?: AgentChatHandlers & {
signal?: AbortSignal;
}): Promise<AgentChatResult>;
}
export declare function createAgentChatClient(options?: AgentChatClientOptions): AgentChatClient;
//# sourceMappingURL=chat-client.d.ts.map