Streaming Events
runtime.query(prompt, thread_id=...) and runtime.run_specialist(specialist, prompt, thread_id=...) both return an async iterator over wire-shape event dicts. Each event is a...
runtime.query(prompt, thread_id=...) and runtime.run_specialist(specialist, prompt, thread_id=...) both return an async iterator over wire-shape event
dicts. Each event is a plain Python dict with a type field discriminator and
event-specific payload fields.
The events follow the AI SDK Data Stream Protocol. The exact shapes are
defined in the Rust agent-fw-core crate and serialised in camelCase.
Consumer loop
A typical consumer dispatches on event["type"]:
async for event in runtime.query("Draft a tiny pricing scenario.", thread_id="thread-1"):
kind = event["type"]
if kind == "text":
print(event["text"], end="")
elif kind == "tool-invocation":
if event["state"] == "call":
print(f"\n[call {event['toolName']}({event['args']})]")
else:
print(f"\n[result {event['toolName']} -> {event['result']}]")
elif kind == "approval-required":
await runtime.respond_to_approval(event["data"]["id"], "approve")
elif kind == "finish":
print(f"\n[done; usage={event['usage']}]")Event kinds
The kinds below are the ones a harness consumer is likely to see. Field names are camelCase on the wire. Optional fields are absent when not applicable.
text
Incremental text token.
{"type": "text", "text": "Hello"}reasoning
Chain-of-thought reasoning, emitted only by models that surface it.
{"type": "reasoning", "text": "..."}step-start
Boundary marker preceding text / reasoning / tool calls for one logical step. Useful for re-grouping output by step.
{"type": "step-start"}tool-invocation
Emitted twice per tool call: once with state == "call" and once with
state == "result". For approval-gated tools the approval-required and
approval-decision events sit between the two.
# state=call
{
"type": "tool-invocation",
"toolInvocationId": "scripted-tool-1",
"toolName": "echo",
"args": {"value": "hello"},
"state": "call",
}
# state=result
{
"type": "tool-invocation",
"toolInvocationId": "scripted-tool-1",
"toolName": "echo",
"args": {"value": "hello"},
"state": "result",
"result": {"echo": "hello"},
}tool-progress
Progress milestone for a long-running tool. Includes a monotonic
phaseIndex, the totalPhases, an optional milestone payload, and the
correlating toolName (plus optional toolCallId).
{
"type": "tool-progress",
"toolName": "buildPlan",
"label": "Resolving products",
"phaseIndex": 1,
"totalPhases": 4,
"milestone": {"matched": 142},
}tool-agent
Sub-agent invocation event, emitted in call / result pairs analogous to
tool-invocation. Carries agentName and the prompt / result payload.
{
"type": "tool-agent",
"agentName": "planner",
"state": "call",
# ...
}data-tool-agent
Sub-agent completion with usage metrics for that agent's slice of the call.
The payload carries agentName, the model it ran on, and a usage object.
{
"type": "data-tool-agent",
"data": {"agentName": "planner", "model": "claude-haiku-4-5", "usage": {...}},
}approval-required
The runtime is waiting for a host decision. See Approvals for the full payload shape and how to respond.
{
"type": "approval-required",
"data": {
"id": "apr-1234",
"kind": "plan",
"target": "demo-plan-1",
"payload": {...},
"resourceId": "acme",
"threadId": "thread-1",
}
}approval-decision
Emitted immediately after runtime.respond_to_approval(...) is processed.
{
"type": "approval-decision",
"data": {
"id": "apr-1234",
"outcome": {"outcome": "approve"},
"feedback": "approved by smoke test"
}
}outcome matches the Rust ApprovalOutcome shape: {"outcome": "approve"},
{"outcome": "reject"}, or {"outcome": "revise", "partial": {...}}.
plan-status-change
Plan lifecycle transition. from and to are canonical status strings
("draft", "approved", "executing", "executed", "failed") or the
"pending_approval" display alias.
{
"type": "plan-status-change",
"data": {
"planId": "demo-plan-1",
"from": "draft",
"to": "pending_approval",
}
}data-file-registered
A file produced by a tool becomes available for download.
{"type": "data-file-registered", "data": {...}}data-cost-summary and data-latency-summary
Aggregated cost and latency metrics for the stream. Both arrive after
finish.
{"type": "data-cost-summary", "data": {...}}
{"type": "data-latency-summary", "data": {...}}finish
Terminal event for the stream. Carries cumulative usage and a
finishReason. At most one finish per stream. totalTokens is computed as
promptTokens + completionTokens; the cache fields are reported separately.
{
"type": "finish",
"finishReason": "stop",
"usage": {
"promptTokens": 12,
"completionTokens": 8,
"cacheReadInputTokens": 0,
"cacheCreationInputTokens": 0,
"totalTokens": 20,
},
}error
Non-recoverable error. No further events follow.
{"type": "error", "error": {"message": "...", "code": "..."}}custom
Domain-specific event from a tool or product layer. On the wire, type is the
literal string "custom"; the domain identifier travels in the separate
event_type field next to the arbitrary JSON data payload.
{"type": "custom", "event_type": "acme-forecast-refresh", "data": {"runId": "fr-42"}}Note that data-flow-ui is not a custom event. The protocol defines it as
its own typed event carrying a pre-computed UI payload, serialized as
{"type": "data-flow-ui", "data": {"dsl": "..."}}. It only appears in a
stream when a product layer emits it; the harness runtime does not produce it
on its own.
Ordering guarantees
The runtime guarantees the following partial order within a stream:
step-startprecedes anytext/reasoningfor that step.- For a tool call:
tool-invocation(call) precedestool-progress* precedestool-invocation(result). - For sub-agents:
tool-agent(call) precedestool-agent(result). - For an approval-gated tool:
tool-invocation(call) precedesapproval-requiredprecedesapproval-decisionprecedestool-invocation(result). finishis terminal; onlydata-cost-summaryanddata-latency-summarymay follow.- After
error, no further events are emitted.
Common errors
| Symptom | Explanation |
|---|---|
| Text never appears before a tool call | Tool-first steps are valid. Dispatch by event["type"], not by assuming text arrives first. |
Stream stops at approval-required | Send runtime.respond_to_approval(...); the runtime is intentionally paused. |
| Usage or cost metadata is missing in the middle of a stream | Read until finish; aggregate summaries arrive at the end. |
| UI treats unknown events as fatal | Preserve or ignore unknown event types so product-specific custom events can pass through. |
See also
Final-Response Judge Evals
Use define_final_response_eval(...) when the runtime's final user-facing text is part of the product outcome. This is common for action-taking agents: action scorers verify what...
Reference
Auto-generated API reference for the flowai_harness public surface. Each page below renders the module's exported classes, functions, and type aliases directly from their...
