Streaming
Display AI responses in real-time as they are generated
Stream agent responses token-by-token — print to the terminal, or iterate tokens yourself to render them in a browser, webview, mobile app, or server.
To stop a stream mid-flight, pass an AbortSignal — see Cancellation.
Which Mode Do I Want?
Pick the surface that matches where the answer renders.
Quick Start
- Iterate tokens with stream()
Render tokens live anywhere — the
for awaitloop pulls them at its own pace.typescript import { Agent } from 'praisonai'; const agent = new Agent({ instructions: 'You are a storyteller' }); for await (const token of agent.stream('Tell me a short story about a robot')) { process.stdout.write(token); // or send to a WebSocket, React state, etc. } - Terminal default (start / chat)
On the CLI, streaming is already on — tokens print to the terminal.
typescript import { Agent } from 'praisonai'; const agent = new Agent({ instructions: 'You are a helpful assistant' }); await agent.chat('Tell me a story about a robot'); // Response streams to the console word by word - Structured events with streamEvents()
Same agent, structured events — handle text, finish, and error.
typescript import { Agent } from 'praisonai'; const agent = new Agent({ instructions: 'You are helpful' }); for await (const event of agent.streamEvents('Hi')) { if (event.type === 'text') { process.stdout.write(event.delta); } else if (event.type === 'finish') { console.log('\n\nDone. Full text:', event.text); } else if (event.type === 'error') { console.error('Stream failed:', event.error); } } - Streaming with Tools
Tool calls are woven into the same stream — text the model speaks before the tool (
"Let me check."), the tool call itself, and the answer after it all arrive as they happen. This holds on every provider: OpenAI, Anthropic, Google, xAI, Groq, and any AI-SDK backend. Pre-tool commentary is delivered as tokens, not held back until the tool finishes.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: 'anthropic/claude-3-5-sonnet-latest', // non-OpenAI provider stream: true, tools: [getWeather] }); await agent.chat('What is the weather in Paris?'); // Console: // Let me check the weather. <-- streamed reasoning // [getWeather("Paris") runs] <-- tool executes // It is 22°C in Paris. <-- streamed final answerIterate
agent.stream()to observe the pre-tool text in a non-console caller — eachchunkis a token string.typescript for await (const chunk of agent.stream('What is the weather in Paris?')) { process.stdout.write(chunk); } - Consume Tokens Yourself
Consume tokens yourself with
agent.stream()— an async iterator over chunks.typescript import { Agent } from 'praisonai'; const agent = new Agent({ instructions: 'You are a helpful assistant' }); for await (const chunk of agent.stream('Tell me a story about a robot')) { process.stdout.write(chunk); } - Stop a Stream
Break the loop to stop the request — the provider stops generating and billing immediately.
typescript import { Agent } from 'praisonai'; const agent = new Agent({ instructions: 'You are a helpful assistant' }); let tokens = 0; for await (const chunk of agent.stream('Write a very long essay')) { process.stdout.write(chunk); if (++tokens === 50) break; // Auto-aborts the underlying request } console.log(agent.lastStopReason); // 'cancelled'For a UI Stop button, pass an
AbortSignal:typescript const controller = new AbortController(); document.getElementById('stop')!.onclick = () => controller.abort(); try { for await (const chunk of agent.stream('Write a long essay', { signal: controller.signal })) { process.stdout.write(chunk); } } catch (err) { if (agent.lastStopReason === 'cancelled') { console.log('User stopped the stream'); } else { throw err; // Real error } }
Cancellation
Break out of the loop to stop the stream cleanly — no more tokens are queued and the in-flight provider request is aborted.
let received = '';
for await (const token of agent.stream('Write a very long essay')) {
received += token;
if (received.length > 500) break; // stops the stream cleanly
}You can also pass an AbortSignal to cancel a turn from outside the loop.
const controller = new AbortController();
setTimeout(() => controller.abort(), 2000); // stop after 2s
for await (const token of agent.stream('Long essay', { signal: controller.signal })) {
process.stdout.write(token);
}How It Works
When tools are configured, the round routes through streamText on every provider — text spoken before the tool call streams as tokens, then the tool runs, then the answer after it streams too.
Streaming with Tools
Text deltas, tool calls, and tool results interleave on a single loop — the stream never stops just because the agent needs a tool.
Bounding the loop
Two knobs bound the run when the model chains many tools.
| Option | Type | Default | Description |
|---|---|---|---|
maxIterations | number | 20 | Maximum tool-call round-trips before the run aborts. |
maxToolCallsPerTurn | number | 10 | Maximum tool calls executed in a single round; extras in one round are dropped with a warning. |
const agent = new Agent({
instructions: 'Research assistant',
stream: true,
tools: [search, fetchUrl, summarise],
maxIterations: 30, // more room for long research chains
maxToolCallsPerTurn: 5, // but at most 5 parallel tools per turn
});Stop a stream
Two ways to cancel a streaming turn — both stop the upstream provider request so no further tokens are generated or billed.
Which one should I use?
| You want to… | Use | Why |
|---|---|---|
| Stop reading (loop-driven) | break in the for await | Natural JS pattern — no plumbing needed |
| Wire a UI Stop button | { signal: controller.signal } | External event fires abort(), cancels the turn |
| Cancel the whole agent (not just this turn) | SimpleAgentConfig.signal | Agent-lifetime — outlives one call |
Tell a Stop from a real error
agent.lastStopReason reports why the last run ended:
| Value | Meaning |
|---|---|
'completed' | Ran to the end |
'cancelled' | User-initiated abort (Stop button, break, or opts.signal) |
'error' | Genuine failure (network, provider error, thrown tool) |
'max_steps' | Hit the tool-loop step cap |
null | Hasn’t run yet |
try {
for await (const chunk of agent.stream('...', { signal })) { /* ... */ }
} catch (err) {
if (agent.lastStopReason === 'cancelled') {
// Silent — user asked for it
} else {
reportError(err); // Real failure
}
}AgentEvent Reference
streamEvents() yields a discriminated union. Switch on event.type.
| Event | Fields | When |
|---|---|---|
text | { type: 'text'; delta: string } | Each incremental token |
tool_call | { type: 'tool_call'; callId: string; name: string; args: Record<string, unknown> } | A tool is about to run |
tool_result | { type: 'tool_result'; callId: string; name: string; ok: boolean; output: string } | A tool finished (ok signals success) |
finish | { type: 'finish'; text: string } | Once, at the end of a successful run — carries the full response |
error | { type: 'error'; error: Error } | Once, if the run threw |
AgentStreamOptions Reference
Both stream() and streamEvents() accept an optional second argument.
| Option | Type | Default | Description |
|---|---|---|---|
previousResult | string | undefined | Substituted for the {{previous}} placeholder in instructions (same semantics as the second arg to start()) |
signal | AbortSignal | undefined | Turn-scoped cancellation. Aborting it stops the provider request so no further tokens are generated or billed |
Configuration Options
The stream config option controls the terminal default and whether token deltas are produced.
| Option | Type | Default | Description |
|---|---|---|---|
stream | boolean | true | Enable streaming responses |
verbose | boolean | false | Show formatted output while streaming |
tools | Function[] | undefined | Tools available during streaming; text and tool calls interleave |
maxIterations | number | 20 | Cap tool-call round-trips in a streaming+tools run |
maxToolCallsPerTurn | number | 10 | Cap tool calls executed in a single round |
agent.stream(prompt, { signal }) | AbortSignal | undefined | Turn-scoped abort signal for the streaming call; aborting it stops the underlying provider request |
Common Patterns
Stream to a Server-Sent Events endpoint
import { Agent } from 'praisonai';
import express from 'express';
const agent = new Agent({ instructions: 'You are helpful' });
const app = express();
app.get('/chat', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
for await (const token of agent.stream(String(req.query.q))) {
res.write(`data: ${token}\n\n`);
}
res.end();
});
app.listen(3000);Log the full text once with streamEvents()
import { Agent } from 'praisonai';
const agent = new Agent({ instructions: 'You are helpful' });
for await (const event of agent.streamEvents('Explain streaming in one line')) {
if (event.type === 'text') process.stdout.write(event.delta);
if (event.type === 'finish') console.log('\n[full]', event.text);
}Terminal default (unchanged)
import { Agent } from 'praisonai';
const writer = new Agent({
instructions: 'You are a creative writer',
stream: true
});
// See the story unfold in the terminal as it is written
await writer.chat('Write a 500-word story about a time-traveling chef');Seeing Tool Activity
If your UI needs to render tool calls and their results — not just the streamed text — use agent.streamEvents() instead of agent.chat(). It yields a typed union (text, tool_call, tool_result, finish, error) in the order the agent produced them, so a call is announced before it runs and its result carries an explicit ok field.
Best Practices
Use stream() for UI hosts
Anywhere tokens must land in a DOM, SSE, WebSocket, or React Native view, for await (const token of agent.stream(...)) gives you each token to route yourself.
Use streamEvents() when you need finish/error semantics
Switch to streamEvents() to close a socket, log the full text once, react to tool activity, or fall back on error — the finish, error, tool_call, and tool_result events carry that context.
Break the loop to cancel
No separate AbortController is needed for the streaming surface itself — breaking out of the for await loop detaches the token sink and aborts the in-flight request. Pass opts.signal only when you need to cancel from outside the loop.
Streaming works with all providers — including with tools
Every supported provider streams — OpenAI, Anthropic, Google, xAI, Groq, and any AI-SDK backend. That includes streaming while tools are running: text the model speaks before a tool_call is delivered as tokens, not held back until the tool finishes. Setting stream: false still works and collapses the round into a single non-streaming call.
Keep stream: true when you use tools on non-OpenAI providers
On Anthropic, Google, xAI, Groq, and any AI-SDK backend, stream: true gives you pre-tool commentary as tokens. Setting stream: false collapses the round into one non-streaming call and delivers the same text at the end in one lump.
Break or abort — both stop billing
Agent.stream() owns the upstream request and cancels it in the iterator’s finally. Breaking a for await loop, or aborting a signal you passed via agent.stream(prompt, { signal }), both stop provider tokens from being generated. Use agent.lastStopReason === 'cancelled' to distinguish a user Stop from a real failure in your error handler.
Non-streaming paths still work with stream()
Even when tools or outputSchema disable token deltas, stream() yields the full response as a single item, so callers always receive an answer.
Keep the CLI default alone
Omitting the token sink (i.e. calling start() or chat()) still writes to process.stdout, so existing CLI scripts don’t change.
Beyond Text: Structured Events
For UIs that need to show tool activity live, use streamEvents() — it yields typed AgentEvents instead of just tokens.
import { Agent } from 'praisonai';
const agent = new Agent({ instructions: 'You are a helpful assistant' });
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[tool] ${event.name}`); break;
case 'tool_result': console.log(`[tool] ${event.ok ? 'ok' : 'fail'}`); break;
case 'finish': console.log(`\n[done]`); break;
}
}Stream tool calls, tool results, and text as structured events