Streaming
Stream text responses in real time from ADK-TS agents using textStreamFrom, collectTextFrom, and streamTextWithFinalEvent — purpose-built utilities for partial event handling.
ADK-TS agents emit text incrementally as the LLM generates it. Rather than waiting for the complete response, your application can display each fragment the moment it arrives. ADK-TS ships three utility functions in @iqai/adk/utils that abstract the partial flag and event iteration so you can work with a clean string stream.
How streaming events work
When the LLM is mid-generation, the runner emits Event objects with partial: true. Each partial event contains a text fragment in event.content.parts. Once generation finishes, a final event arrives with partial unset (or false) — this is the event isFinalResponse() returns true for.
Event { partial: true, content: { parts: [{ text: "The answer" }] } } ← delta
Event { partial: true, content: { parts: [{ text: " is 42." }] } } ← delta
Event { partial: false, content: { parts: [{ text: "The answer is 42." }] } } ← final (full text)You can handle partial events manually, but the streaming utilities make the common cases much simpler.
textStreamFrom
textStreamFrom converts a runner.runAsync() generator into a generator that yields only the raw text deltas. Use it when you need to print or display each word as it arrives and don't need any other event metadata:
import { AgentBuilder } from "@iqai/adk";
import { textStreamFrom } from "@iqai/adk/utils";
const { runner } = await AgentBuilder.create("assistant")
.withModel("gemini-2.5-flash")
.withInstruction("You are a helpful assistant.")
.build();
const events = runner.runAsync({
userId: "user-1",
sessionId: "session-1",
newMessage: { role: "user", parts: [{ text: "Tell me about black holes." }] },
});
for await (const text of textStreamFrom(events)) {
process.stdout.write(text); // prints each chunk immediately
}textStreamFrom only yields text from partial: true events, so it silently skips function calls, function responses, and the non-streaming final event.
collectTextFrom
collectTextFrom accumulates all text deltas and resolves to the complete response string once the stream ends. Use it when you want the streaming benefit of low first-byte latency from the model but only need the final string in your code:
import { collectTextFrom } from "@iqai/adk/utils";
const events = runner.runAsync({
userId: "user-1",
sessionId: "session-1",
newMessage: { role: "user", parts: [{ text: "What is TypeScript?" }] },
});
const fullText = await collectTextFrom(events);
console.log(fullText); // complete responsestreamTextWithFinalEvent
streamTextWithFinalEvent combines streaming and full event access in one call. It returns a textStream async generator for progressive display and a finalEvent promise that resolves with the last event once streaming completes — giving you both the text stream and the metadata (usage stats, tool calls, state deltas) from the final event:
import { streamTextWithFinalEvent } from "@iqai/adk/utils";
const events = runner.runAsync({
userId: "user-1",
sessionId: "session-1",
newMessage: { role: "user", parts: [{ text: "Calculate 17 × 23." }] },
});
const { textStream, finalEvent } = streamTextWithFinalEvent(events);
// Stream the text as it arrives
for await (const text of textStream) {
process.stdout.write(text);
}
// Inspect the final event for metadata
const final = await finalEvent;
if (final) {
console.log("\nState changes:", final.actions.stateDelta);
console.log("Tool calls:", final.getFunctionCalls());
}finalEvent resolves after the textStream generator completes, so always
await the finalEvent after finishing the for await loop — not before.
Choosing the right utility
| Utility | Use when |
|---|---|
textStreamFrom | You need progressive display and nothing else |
collectTextFrom | You need the complete string; don't need per-chunk display |
streamTextWithFinalEvent | You need progressive display AND metadata from the final event |
Raw runner.runAsync() | You need to handle tool calls, state changes, or non-text events too |
Manual streaming loop
When you need finer control — for example, to handle both streaming text and tool events in the same loop — work with event.partial directly:
let buffer = "";
for await (const event of runner.runAsync(request)) {
// Accumulate streaming chunks
if (event.partial && event.content?.parts?.[0]?.text) {
buffer += event.content.parts[0].text;
updateUI(buffer); // show partial text in real time
continue;
}
// Handle function calls while streaming
if (event.getFunctionCalls().length > 0) {
showToolActivity(event.getFunctionCalls());
continue;
}
// Final response — the final event carries the full accumulated text,
// so use it directly rather than appending to the buffer (which already
// holds all the deltas and would duplicate the content).
if (event.isFinalResponse()) {
const text = event.content?.parts?.[0]?.text ?? buffer;
finalizeUI(text);
buffer = "";
}
}Next steps
Event Compaction
Automatically summarise long ADK-TS session histories with LLM-generated compaction events so context windows stay manageable without losing key information.
Event Patterns
Practical ADK-TS event patterns — chat loops, tool activity indicators, agent transfer tracking, state replay, and audit logs built on the real framework APIs.