@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
117 lines • 5.31 kB
JavaScript
/** An event plus fields outside the shared vocabulary (allowed: readers ignore what they do not know). */
function withExtra(event, extra) {
return { ...extra, ...event };
}
/**
* A reconnect cursor from `?after=` or the `Last-Event-ID` header: a non-negative integer, or
* `undefined` for anything else (absent, malformed).
*/
export function parseStreamCursor(raw) {
const value = Array.isArray(raw) ? raw[0] : raw;
if (typeof value !== 'string' || !/^\d+$/.test(value.trim())) {
return undefined;
}
const parsed = Number(value.trim());
return Number.isSafeInteger(parsed) ? parsed : undefined;
}
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* The agent-protocol events one {@link StreamFrame} stands for. Pure: the same frame at the same
* position always yields the same events, which is what lets a re-attaching reader number them the
* way the first reader did.
*
* `position` is the frame's 0-based index in the run's buffer; it names a component frame buffered
* before the loop stamped ids on them (`ui:<position>`), so a replay replaces rather than duplicates.
*/
export function frameToEvents(frame, position) {
switch (frame.t) {
case 'text':
return frame.v.length > 0 ? [{ kind: 'text', text: frame.v }] : [];
case 'event':
return [frame.event];
case 'component':
return [
{
kind: 'ui',
id: frame.id ?? `ui:${position}`,
component: frame.name,
props: isRecord(frame.data) ? frame.data : { value: frame.data },
...(frame.version !== undefined ? { version: frame.version } : {}),
...(frame.fallbackText !== undefined ? { fallbackText: frame.fallbackText } : {}),
...(frame.componentVersions !== undefined
? { componentVersions: frame.componentVersions }
: {}),
...(frame.toolCallId !== undefined ? { toolCallId: frame.toolCallId } : {}),
...(frame.partial === true ? { partial: true } : {}),
},
];
case 'elicitation':
// `runId` rides along (fields are only ever added): a delegated sub-agent's form is answered
// against the CHILD run, which is not the stream it arrived on.
return [
withExtra({ kind: 'elicitation', id: frame.id, request: frame.request }, { runId: frame.runId }),
];
case 'approval':
// `approver: 'requester'` — the person chatting (or a governance-privileged actor) decides.
// `runId`/`toolName`/`input` ride along for a reader that decides without the call's frames.
return [
withExtra({
kind: 'approval-requested',
id: frame.id,
...(frame.target !== undefined ? { target: frame.target } : {}),
approver: frame.approver ?? 'requester',
...(frame.confirmation !== undefined ? { confirmation: frame.confirmation } : {}),
...(frame.expiresAt !== undefined ? { expiresAt: frame.expiresAt } : {}),
}, { runId: frame.runId, toolName: frame.toolName, input: frame.input }),
];
case 'error':
return [];
}
}
/**
* Writes a run's frames in the agent protocol, one `data:` frame per event. Stateful only in the
* positions it counts, so feed it every frame of the run in buffer order.
*
* Every event carries an SSE `id:` — its 1-based sequence number within the run. The number is a
* pure function of the run's buffered stream (every sink replays a run from its first frame, in
* write order, and {@link frameToEvents} is pure), so the same event gets the same number on the
* POST that started the run and on any later `GET …/stream`, whichever replica serves it. Events at
* or below `after` — what a reconnecting client already has — are counted but not written. `meta`,
* `done` and `error` carry no id.
*/
export class AgentSseEncoder {
after;
position = 0;
seq = 0;
failed = false;
constructor(after = 0) {
this.after = after;
}
/** The SSE text for the next frame of the run (possibly `''`). */
encode(frame) {
const position = this.position;
this.position += 1;
if (frame.t === 'error') {
this.failed = true;
return `event: error\ndata: ${JSON.stringify({ code: frame.code, message: frame.message })}\n\n`;
}
let out = '';
for (const event of frameToEvents(frame, position)) {
this.seq += 1;
if (this.seq > this.after) {
out += `id: ${this.seq}\ndata: ${JSON.stringify(event)}\n\n`;
}
}
return out;
}
/**
* The closing frame: `event: done` for a run that ended, nothing after an `event: error` — a
* reader takes the error as the end, and a `done` after it would read as a success.
*/
close() {
return this.failed ? '' : 'event: done\ndata: {}\n\n';
}
}
//# sourceMappingURL=sse.js.map