Stream Events
Consume structured text and tool events from agent.streamEvents()
agent.streamEvents() yields a typed union ā text tokens, tool calls, tool results, and terminal events ā in the exact order the agent produced them, so a UI can show tool activity instead of inferring it from prose.
Quick Start
- Consume the event stream
One agent, one tool, one
for awaitā switch onevent.type.typescript import { Agent } from 'praisonai'; function getWeather(city: string): string { return `Weather in ${city}: 22°C, sunny`; } const agent = new Agent({ instructions: 'You are a weather assistant', llm: 'gpt-4o-mini', stream: true, tools: [getWeather], }); for await (const event of agent.streamEvents('What is the weather in Paris?')) { switch (event.type) { case 'text': process.stdout.write(event.delta); break; case 'tool_call': console.log(`\nā ${event.name}(${JSON.stringify(event.args)})`); break; case 'tool_result': console.log(` ${event.ok ? 'ā' : 'ā'} ${event.output}`); break; case 'finish': console.log(`\n[done]`); break; case 'error': console.error(event.error); break; } }
The Event Union
streamEvents() yields the AgentEvent discriminated union ā five variants, keyed on type.
| Variant | Fields | Emitted when |
|---|---|---|
text | delta: string | The model emits a token. |
tool_call | callId: string, name: string, args: Record<string, unknown> | A tool is about to run. Announced before it runs so a UI can render a call-in-progress row. |
tool_result | callId: string, name: string, ok: boolean, output: string | A tool finished, threw, or was denied by the approval gate. |
finish | text: string | The run completed. |
error | error: Error | The run errored out. |
Pairing Calls to Results with callId
callId is the provider’s id for the invocation and is the only key that may pair a tool_result to its tool_call.
Matching by position holds only while exactly one call is in flight, and silently attributes the wrong output the moment that stops being true.
const inFlight = new Map<string, { name: string; args: unknown }>();
for await (const event of agent.streamEvents(prompt)) {
if (event.type === 'tool_call') {
inFlight.set(event.callId, { name: event.name, args: event.args });
} else if (event.type === 'tool_result') {
const call = inFlight.get(event.callId);
inFlight.delete(event.callId);
renderRow(call!, event);
}
}Order Matters
Structured events share the same queue as the text, which keeps a tool call in its true position.
On a separate channel a call could arrive before the sentence introducing it, and a reader cannot tell a reordered transcript from a model that genuinely said things in that order.
// A transcript rendered in event order reads exactly as it happened:
//
// Checking. ā text
// ā getWeather({"city":"Paris"}) ā tool_call
// ā Weather in Paris: 22°C, sunny ā tool_result
// It is 22°C and sunny in Paris. ā text
// [done] ā finishstream() vs streamEvents()
Use stream() for text-only output; use streamEvents() when the UI must render tool activity.
stream() filters on type === 'text' internally, so widening the AgentEvent union does not affect it ā text-only consumers are unaffected by this change.
Upgrading an Exhaustive switch
Widening a union is source-breaking for an exhaustive switch.
A consumer that handled text/finish/error and treated everything else as an error will now see tool_call/tool_result on the default branch. Add cases (or ignore the new variants explicitly) before upgrading.
for await (const event of agent.streamEvents(prompt)) {
switch (event.type) {
case 'text': process.stdout.write(event.delta); break;
case 'finish': done(event.text); break;
case 'error': fail(event.error); break;
default: /* tool_call, tool_result ā ignore or handle */ break;
}
}Cancelling a Stream
Pass an AbortSignal through AgentStreamOptions.signal to stop generation when the user leaves the screen.
const controller = new AbortController();
// Later, when the user navigates away:
// controller.abort();
for await (const event of agent.streamEvents(prompt, { signal: controller.signal })) {
if (event.type === 'text') process.stdout.write(event.delta);
}Breaking out of the for await loop also aborts automatically ā the iterator’s return() stops the in-flight provider request so no further tokens are generated or billed.
Best Practices
Show ok in the UI, not the output length
The whole point of ok is that output length is not a signal. Render a checkmark or an error state from event.ok, never from whether event.output is non-empty.
Render tool_call immediately
tool_call is emitted before the tool runs. Render a call-in-progress row the moment it arrives so the user knows work is happening.
Match by callId from day one
Position-matching works right up until the day two calls are in flight at once, then it silently attributes the wrong output. Key a Map on callId from the start.
Cancel long streams
Pass an AbortSignal via AgentStreamOptions.signal when the user leaves the screen, or break the for await loop. Both stop the provider request so tokens are no longer generated or billed.