TypeScriptADK-TS

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 response

streamTextWithFinalEvent

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

UtilityUse when
textStreamFromYou need progressive display and nothing else
collectTextFromYou need the complete string; don't need per-chunk display
streamTextWithFinalEventYou 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