> For the complete documentation index, see [llms.txt](/llms.txt). Every page on this site is also available as markdown at `<path>.md`.

# 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 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"]`:

```python
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.

```python
{"type": "text", "text": "Hello"}
```

### `reasoning`

Chain-of-thought reasoning, emitted only by models that surface it.

```python
{"type": "reasoning", "text": "..."}
```

### `step-start`

Boundary marker preceding text / reasoning / tool calls for one logical step.
Useful for re-grouping output by step.

```python
{"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.

```python
# 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`).

```python
{
    "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.

```python
{
    "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.

```python
{
    "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](/docs/guides/approvals) for the full payload shape and how to respond.

```python
{
    "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.

```python
{
    "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.

```python
{
    "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.

```python
{"type": "data-file-registered", "data": {...}}
```

### `data-cost-summary` and `data-latency-summary`

Aggregated cost and latency metrics for the stream. Both arrive after
`finish`.

```python
{"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.

```python
{
    "type": "finish",
    "finishReason": "stop",
    "usage": {
        "promptTokens": 12,
        "completionTokens": 8,
        "cacheReadInputTokens": 0,
        "cacheCreationInputTokens": 0,
        "totalTokens": 20,
    },
}
```

### `error`

Non-recoverable error. No further events follow.

```python
{"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.

```python
{"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-start` precedes any `text` / `reasoning` for that step.
- For a tool call: `tool-invocation` (`call`) precedes `tool-progress`* precedes
  `tool-invocation` (`result`).
- For sub-agents: `tool-agent` (`call`) precedes `tool-agent` (`result`).
- For an approval-gated tool: `tool-invocation` (`call`) precedes
  `approval-required` precedes `approval-decision` precedes
  `tool-invocation` (`result`).
- `finish` is terminal; only `data-cost-summary` and `data-latency-summary`
  may 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

- [Approvals](/docs/guides/approvals)
- [Testing](/docs/guides/testing)
- [`Runtime` reference](/docs/reference/runtime#flowai_harness.runtime.Runtime)
