@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
131 lines • 5.79 kB
TypeScript
import type { ElicitationRequest } from '../elicitation.js';
import type { AgentStreamErrorCode, AgentStreamEvent } from '../stream-events.js';
import type { ToolConfirmation } from '../tool-presentation.js';
/**
* The "data plane": live token transport, decoupled from the durable control plane.
*
* The model turn writes typed frames to a `SinkWriter` keyed by runId; the HTTP layer
* `subscribe`s by runId and pipes the frames to the browser as SSE. A late subscriber
* (reconnect/resume) replays buffered frames first, then follows live — which is what
* makes streaming survive a dropped connection or a pod restart.
*/
export type StreamFrame = {
t: 'text';
v: string;
}
/**
* A component a tool pushed (`ctx.emitComponent`). `id` and `toolCallId` are stamped by the loop
* (`<toolCallId>:ui:<n>`) so the agent protocol can carry it as a `ui` frame a repeat replaces;
* a frame buffered before they existed has neither, and is numbered by its position instead.
*/
| {
t: 'component';
name: string;
data: unknown;
id?: string;
toolCallId?: string;
/** Schema version of `data` (`ctx.emitUi`'s `version`). */
version?: number;
fallbackText?: string;
componentVersions?: Record<string, number>;
/** A preview of a call's streaming input (`AgentUiComponent.partial`): never persisted. */
partial?: true;
}
/**
* One frame of the agent stream protocol (`AgentStreamEvent`, shared with `@dudousxd/nestjs-agent`)
* that has no older spelling here: reasoning, tool-call announcements and outcomes, step brackets,
* title, cancel.
*/
| {
t: 'event';
event: AgentStreamEvent;
}
/**
* The run failed. Streamed as `event: error` `{ code, message }`.
*/
| {
t: 'error';
code: AgentStreamErrorCode;
message: string;
}
/**
* A question set the run is now parked on. Carries the whole request, so a client renders the form
* — and knows the total ("Question 1 of 3") — without a second fetch. Identical whether a
* configured intake or the model's `ask` authored it.
*
* `runId` is the run that is PARKED, which is not always the stream this frame arrived on: a
* delegated sub-agent forwards its frames into its top-level ancestor's stream so the human can
* see them, and answering it means addressing the CHILD's run. Without the id on the frame a
* watcher has the form and no way to reply to it.
*/
| {
t: 'elicitation';
runId: string;
id: string;
request: ElicitationRequest;
}
/**
* An `action` tool call the run is now parked on, waiting for an approve/reject. Carries the tool
* and the arguments it was asked to run with, so a watcher can decide without a second fetch.
*
* `runId`, like the elicitation frame's, is the run that is PARKED rather than the stream this
* arrived on: a delegated sub-agent's action suspends its OWN run, and forwarding the frame into
* the ancestor's stream is the only way a human sees it — so the id has to ride along, or the
* watcher can see the approval and not make it.
*/
| {
t: 'approval';
target?: {
kind: 'proposal';
proposalId: string;
};
runId: string;
id: string;
toolName: string;
input: unknown;
/** Resolved display copy from the action's domain preparation. */
confirmation?: ToolConfirmation;
/** Who may decide (`'requester'` or a role), from the turn's `ApprovalPolicy`. Absent → the requester. */
approver?: string;
/** ISO-8601 instant the request lapses. Absent → it never does. */
expiresAt?: string;
};
export interface SinkWriter {
write(frame: StreamFrame): void | Promise<void>;
/** Mark the run's stream finished (no more frames). */
end(): void | Promise<void>;
/**
* OPTIONAL: write out anything this writer is still holding back, without ending the stream. Only
* a sink that gathers frames before writing them (the Lucid sink coalesces text) has anything to
* do here; {@link childSinkWriter} calls it where a delegated run would have ended the stream.
*/
flush?(): void | Promise<void>;
}
export interface TokenStreamSink {
/** Open (or reopen) the writer for a run. */
open(runId: string): SinkWriter | Promise<SinkWriter>;
/** Replay buffered frames for the run, then yield live ones until `end()`. */
subscribe(runId: string): AsyncIterable<StreamFrame>;
/** Drop any buffer/resources for the run. */
close(runId: string): void | Promise<void>;
/**
* OPTIONAL: does this sink hold anything for the run — a buffer it opened, frames, an end marker?
* `GET <path>/chat/:runId/stream` answers `404` when it says no, which a client reads as "nothing to
* resume" (the run ended while it was away, or the buffer went with a restarted process) instead of
* waiting forever on a stream nobody will write. Absent → the route subscribes regardless.
*/
has?(runId: string): boolean | Promise<boolean>;
}
/**
* Wrap a {@link SinkWriter} so a DELEGATED run forwards its frames into its top-level ancestor's
* stream but never closes it.
*
* Forwarding is what makes a sub-agent visible at all: its own runId is not a stream anyone
* subscribed to, so a question set it parks on would otherwise be written where nobody reads it.
* The ancestor owns the stream's lifecycle across however many delegations it spans, so the child's
* `end()` is a no-op — ending the shared stream mid-parent-run would cut the human off. It does
* {@link SinkWriter.flush} what the child wrote, so nothing of the delegate's is still held back when
* its parent (possibly on another replica) writes the next frame.
*/
export declare function childSinkWriter(inner: SinkWriter): SinkWriter;
//# sourceMappingURL=token-stream-sink.d.ts.map