@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
90 lines • 3.77 kB
JavaScript
import { AgUiEncoder } from './encoder.js';
/** The run's own sequence number of each source frame tagged by {@link withFrameSeq}. */
const frameSeqs = new WeakMap();
/**
* Tag a source frame with the run's own sequence number through it — the SSE `id:` of the last
* event the native stream writes for it — for {@link AgUiStreamOptions.cursor}.
*/
export function withFrameSeq(frame, seq) {
frameSeqs.set(frame, seq);
return frame;
}
/** The run's own sequence number of a frame tagged by {@link withFrameSeq}, if any. */
export function frameSeq(frame) {
return frameSeqs.get(frame);
}
const DEFAULT_QUIET_MS = 750;
const GRACE_MS = 50;
/**
* One AG-UI run over a library run's stream.
*
* AG-UI has no mid-run channel from the consumer: a run that needs an approval or an answer does not
* wait with the connection open — it ENDS, saying what it waits for, and a later run carries the
* answers. The library run underneath does wait (parked, durably when the runner is durable). So this
* stops reading when the run is waiting on someone and nothing else is moving, closes the AG-UI run
* with the interrupt outcome, and leaves the library run parked for the resume to pick up.
*/
export async function* agUiEvents(frames, options) {
const encoder = new AgUiEncoder(options);
const quietMs = options.quietMs ?? DEFAULT_QUIET_MS;
yield* encoder.start();
yield* options.preamble ?? [];
const iterator = frames[Symbol.asyncIterator]();
let upcoming;
let ended = false;
try {
for (;;) {
upcoming ??= iterator.next();
// Frames buffered before this reader attached are replayed without a pause between them, so
// "is it waiting?" is only asked of a stream that has caught up with what it already holds.
const caughtUp = encoder.consumed >= (options.skip ?? 0);
let next;
if (caughtUp && encoder.waiting) {
// Nothing else in flight: only a frame already on its way (a sink that reads in batches)
// can still arrive, so a short grace is enough. Otherwise wait out the quiet window.
next = await raceQuiet(upcoming, encoder.busy ? quietMs : Math.min(quietMs, GRACE_MS));
}
else {
next = await upcoming;
}
if (next === 'quiet')
break;
upcoming = undefined;
if (next.done === true) {
ended = true;
break;
}
if (options.cursor !== undefined) {
options.cursor.seq = frameSeq(next.value) ?? options.cursor.seq;
}
yield* encoder.encode(next.value);
}
}
finally {
// Stop following the library stream; the run it carries is left exactly as it is.
void iterator.return?.();
}
yield* encoder.finish(ended);
}
function raceQuiet(upcoming, ms) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve('quiet'), ms);
upcoming.then((result) => {
clearTimeout(timer);
resolve(result);
}, (error) => {
clearTimeout(timer);
reject(error);
});
});
}
/**
* One AG-UI event as an SSE frame: a single `data:` line, LF-terminated, as the binding pins. With a
* positive `id`, an SSE `id:` line first — the run's sequence number ({@link AgUiStreamOptions.cursor}),
* which AG-UI clients ignore and this package's React client re-attaches with.
*/
export function agUiSse(event, id) {
const head = id !== undefined && id > 0 ? `id: ${id}\n` : '';
return `${head}data: ${JSON.stringify(event)}\n\n`;
}
//# sourceMappingURL=stream.js.map