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.
The events
Section titled “The events”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.
Streaming text
Section titled “Streaming text”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.
Reacting to tool calls
Section titled “Reacting to tool calls”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.
Completion and errors
Section titled “Completion and errors”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'.
Outputs persist on the prompt
Section titled “Outputs persist on the prompt”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.