Guides

Streaming

Consuming an agent run as live events with .stream().

agent.stream({ prompt }) runs the exact same loop as .generate() (see Agents), but instead of silently draining it and returning only the final result, it exposes every lifecycle event live as an AsyncGenerator<AgentEvent>:

import { Agent, tool, type AgentEvent } from "chai-ai";
import { google } from "@chai-ai/google";
import { z } from "zod";

const calculator = tool({
  description: "Performs a single arithmetic operation on two numbers.",
  inputSchema: z.object({
    operation: z.enum(["add", "multiply"]),
    a: z.number(),
    b: z.number(),
  }),
  execute({ operation, a, b }) {
    return operation === "add" ? a + b : a * b;
  },
});

const agent = new Agent({
  name: "calculator-agent",
  instructions: "Always use the calculator tool for arithmetic instead of computing it yourself.",
  model: google("gemini-3.1-pro-preview"),
  tools: { calculator },
});

for await (const event of agent.stream({ prompt: "What is 47 multiplied by 23?" })) {
  handleEvent(event);
}

Because .generate() and .stream() share one engine, switching between them doesn't change agent behavior — only whether you see intermediate steps as they happen.

Event types

AgentEvent (chai-ai) is a discriminated union on type:

EventWhen
run_startedOnce, at the very start of the run
text_deltaIncremental text chunks as the model streams its response
tool_startedRight before a tool call executes (toolName, args)
tool_completedAfter a tool call finishes (result, or error)
handoff_startedWhen control transfers to another agent — see Handoffs
guardrail_triggeredWhen an input/tool/output guardrail blocks, requires approval, or resolves — see Guardrails
run_completedOnce, at the end of a successful run — carries the final text/output
run_failedOnce, if the run throws — carries error

The final event yielded is always run_completed or run_failed, which already carries the finished text / output / error, so there's no separate result-retrieval step after the loop ends.

Handling events

function handleEvent(event: AgentEvent) {
  switch (event.type) {
    case "text_delta":
      process.stdout.write(event.delta);
      break;
    case "tool_started":
      console.log(`\n[tool_started] ${event.toolName}(${JSON.stringify(event.args)})`);
      break;
    case "tool_completed":
      console.log(`[tool_completed] ${event.toolName} -> ${JSON.stringify(event.result)}`);
      break;
    case "handoff_started":
      console.log(`\n[handoff_started] ${event.fromAgent} -> ${event.toAgent}`);
      break;
    case "guardrail_triggered":
      console.log(`\n[guardrail_triggered] ${event.guardrailName}: ${event.action}`);
      break;
    case "run_completed":
      console.log(`\n[run_completed] runId=${event.runId}`);
      break;
    case "run_failed":
      console.log(`\n[run_failed] ${event.error}`);
      break;
    // run_started: not logged, just marks the beginning of the run.
  }
}

See the runnable version of this example in examples/streaming.

On this page