UNPKG

@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
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