Skip to content

Stream output and react to events

An AgentRun is an event emitter. As it drives the loop it emits events for every piece of output it produces, so you can stream text to a UI, react to tool calls as they happen, and know when the run finishes. This guide covers each event and shows how to consume them.

Subscribe with run.on(name, handler) before calling run():

Event Payload When
output:added { prompt, output } A new output entry (text, tool call, or item) is appended to the prompt.
output:delta { prompt, outputId, delta } A chunk of streaming text arrived for the text output with outputId.
output:updated { prompt, output } An existing output entry changed — text finished streaming, a tool call recorded its result, an interrupt was resolved.
interrupted { prompt, message, data } An action threw ActionInterrupt and the run suspended.
completed prompt The model stopped requesting tools and the run finished.
error error The run failed with a non-interrupt error (the error is also thrown from run()).

Every output in these payloads is the same object stored on prompt.output, so the events are a live view of the transcript being built. See The prompt.

output:delta fires once per text chunk. Concatenate the deltas to assemble the reply, or write each straight to a stream:

run.on('output:delta', ({ delta }) => {
process.stdout.write(delta);
});

If you keep more than one text output around (a run can produce several), key your buffer by outputId:

const buffers = new Map<string, string>();
run.on('output:delta', ({ outputId, delta }) => {
buffers.set(outputId, (buffers.get(outputId) ?? '') + delta);
});

A new text output is announced with output:added before its first delta, and output:updated fires once the text has finished streaming — useful if you would rather work with the finished string than assemble chunks.

Tool calls arrive as output entries of type 'tool'. Watch output:added to see a call start, and output:updated to see its result (or error) land:

run.on('output:added', ({ output }) => {
if (output.type === 'tool') {
console.log(`calling ${output.name}`, output.input);
}
});
run.on('output:updated', ({ output }) => {
if (output.type === 'tool' && output.output !== undefined) {
console.log(`${output.name} returned`, output.output);
}
});

A tool call entry carries its input, and after execution either an output or an error. If the action suspended the run, the entry instead gains an interrupts history — see Handle interrupts.

completed fires with the finished prompt when the model stops requesting tools. error fires if the run throws for any reason other than an interrupt:

run.on('completed', (prompt) => {
console.log('done —', prompt.output.length, 'outputs');
});
run.on('error', (error) => {
console.error('run failed', error);
});

The error event mirrors what run() rejects with, so you can handle failures via events, try/catch, or both. An ActionInterrupt is not an error — it surfaces as interrupted and resolves run() with status: 'interrupted'.

Events are convenient for reacting in real time, but nothing is lost if you ignore them. Every output the run emits is also appended to prompt.output on the active prompt. After run() resolves you can read the whole transcript from the result:

const result = await run.run();
for (const output of result.prompt.output) {
if (output.type === 'text') {
console.log('text:', output.text);
} else if (output.type === 'tool') {
console.log('tool:', output.name, '->', output.output ?? output.error);
}
}

Because the transcript lives on the prompt, you can persist it and reconstruct a run later without having captured any events — the same property that makes interrupts resumable. See The agent run loop.