> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mezmo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Streaming API Guide

> OpenAI-compatible Server-Sent Events (SSE) streaming, custom aura.* events, tool result modes, and orchestration events.

OpenAI-compatible Server-Sent Events (SSE) streaming for real-time responses.

## Quick Start

```bash theme={null}
curl -X POST http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "Hello!"}], "stream": true}'
```

## Configuration

### Tool Result Modes

The server supports three streaming modes, configured via CLI or environment variable:

| Mode | Tool Call Args | Tool Results | Use Case |
| - | - | - | - |
| `none` (default) | Actual JSON | Not streamed | Spec-compliant API clients |
| `open-web-ui` | Empty `""` | Streamed via `tool_calls` | OpenWebUI "View Results" support |
| `aura` | Actual JSON | Via `aura.tool_complete` events | Custom clients with AURA events |

```bash theme={null}
# Spec-compliant mode (default)
cargo run --bin aura -- webserver

# OpenWebUI compatibility mode
cargo run --bin aura -- webserver --tool-result-mode open-web-ui

# AURA events mode (requires AURA_CUSTOM_EVENTS=true)
AURA_CUSTOM_EVENTS=true cargo run --bin aura -- webserver --tool-result-mode aura

# Via environment variable
TOOL_RESULT_MODE=aura AURA_CUSTOM_EVENTS=true cargo run --bin aura -- webserver
```

### Environment Variables

| Variable | Default | Description |
| - | - | - |
| `TOOL_RESULT_MODE` | `none` | `none`, `open-web-ui`, or `aura` |
| `TOOL_RESULT_MAX_LENGTH` | `1000` | Max chars for tool results (0 = no truncation) |
| `STREAMING_TIMEOUT_SECS` | `900` | Request timeout in seconds (0 = no timeout) |
| `FIRST_CHUNK_TIMEOUT_SECS` | `90` | Max seconds to wait for first provider chunk before aborting |
| `STREAMING_BUFFER_SIZE` | `400` | Chunks to buffer before backpressure |
| `AURA_CUSTOM_EVENTS` | `false` | Enable optional custom `aura.*` events. HITL approval lifecycle events are emitted regardless because clients may need to act on them. |
| `AURA_EMIT_REASONING` | `false` | Enable `aura.reasoning` events |
| `SHUTDOWN_TIMEOUT_SECS` | `30` | Grace period (seconds) for in-flight streams on shutdown |

## Server Info Endpoint

`GET /aura/info` is an aura-native introspection endpoint. It returns the
default agent and, per agent, its orchestration workers and configured MCP
servers. This endpoint is not OpenAI-compatible; it lives under `/aura/` to keep
`/v1/models` clean.

The CLI uses this at boot to display orchestration workers before the first
prompt in HTTP mode.

```bash theme={null}
curl http://localhost:8080/aura/info | jq
```

```json theme={null}
{
  "default_agent": "orch",
  "agents": [
    {
      "id": "orch",
      "model": "gpt-4o",
      "description": "General-purpose assistant with tool access",
      "workers": [
        { "name": "planner", "description": "Plans work" },
        { "name": "writer", "description": "Writes summaries", "model": "gpt-4o-mini" }
      ],
      "mcp_servers": {
        "logs": {
          "transport": "http_streamable",
          "url": "https://logs.example.com",
          "description": "Search logs."
        },
        "fs": { "transport": "stdio", "command": "fs-server" }
      }
    },
    {
      "id": "solo",
      "model": "gpt-4o",
      "mcp_servers": {}
    }
  ]
}
```

<Note>The `description` on each agent object is the agent's own summary. It is distinct from the `description` inside each `workers` entry, which describes an orchestration worker.</Note>

Each agent's `id` matches the `id` field in `/v1/models` (alias if set,
otherwise agent name). Each agent object can also carry a `description`: the agent's human-readable
summary from its `[agent].description` config (see the [configuration
reference](/aura/configuration-reference#agent)). It is omitted from the response when
that config field is unset. Omitting it when
unset keeps older servers and clients compatible. This is the same value returned by `/v1/models` (see the [web server reference](/aura/web-server-reference)); use `/aura/info` when a client also wants the worker or MCP-server metadata this endpoint provides. The `workers` array is
omitted for non-orchestration agents. Each worker's `model` is included only
when it overrides the coordinator model.

`mcp_servers` is a credential-free view of the agent's configured MCP servers,
keyed by name and tagged by `transport` (`stdio`, `http_streamable`, or `sse`).
URLs are reduced to their origin (`scheme://host:port`): path, query,
fragment, and userinfo are all dropped, since any of them can carry a token. A
stdio server shows only the executable basename as `command`; its arguments
and environment are never included, and neither are `headers` or
`headers_from_request`. A URL that cannot be reduced to an origin appears as
the sentinel `<invalid url>`, and a command with no extractable file name as
`<unknown>`. An empty object means no configured MCP servers, while
a populated one lists them. Older servers omit the field entirely, and the CLI
then uses its generic startup call-to-action.

## Custom AURA Events (Optional)

Most custom AURA events are optional and require `AURA_CUSTOM_EVENTS=true`.
HITL approval lifecycle events are the exception: the server emits them whenever
an approval route needs clients to observe or act on approval state.

```bash theme={null}
AURA_CUSTOM_EVENTS=true cargo run --bin aura -- webserver
```

### Custom Event Types

| Event | Description | Status |
| - | - | - |
| `aura.tool_requested` | LLM decided to call a tool (immediate UI feedback, has arguments) | ✅ Implemented |
| `aura.tool_start` | MCP execution actually begins (has `progress_token` for correlation) | ✅ Implemented |
| `aura.tool_complete` | Tool execution finished (with `duration_ms`, result/error) | ✅ Implemented |
| `aura.reasoning` | LLM reasoning content (requires `AURA_EMIT_REASONING=true`) | ✅ Implemented |
| `aura.progress` | MCP progress notifications during long-running tools | ✅ Implemented |
| `aura.session_info` | Session metadata (model, context window) emitted at stream start | ✅ Implemented |
| `aura.skills_rehydrated` | Skill invocations from earlier turns replayed into this turn's context, emitted at stream start | ✅ Implemented |
| `aura.mcp_status` | Per-server MCP connection status emitted at stream start (connected/failed/not\_attempted, with failure reason) | ✅ Implemented |
| `aura.worker_phase` | Worker phase transitions in multi-agent mode (planning/executing/analyzing) | ✅ Implemented |
| `aura.tool_usage` | Usage snapshot after tool execution (associates tool IDs with token counts) | ✅ Implemented |
| `aura.usage` | Final token usage emitted at stream end (prompt/completion/total) | ✅ Implemented |
| `aura.scratchpad_usage` | Per-agent scratchpad usage summary (single-agent or worker), emitted when an agent finishes with scratchpad activity | ✅ Implemented |
| `aura.approval_requested` | HITL approval request raised for a gated tool or `request_approval` call | ✅ Implemented for webhook and conversational routes |
| `aura.approval_pending` | HITL approval is waiting for an attended decision | ✅ Implemented for conversational route |
| `aura.approval_completed` | HITL approval reached a terminal outcome | ✅ Implemented for webhook and conversational routes |
| `aura.orchestrator.*` | Orchestration lifecycle events (see [Orchestration Events](#orchestration-events) below) | ✅ Implemented |

### Event Flow

```mermaid theme={null}
flowchart TD
    C["MCP execution begins"]
    F["MCP execution ends"]

    A[LLM decides to call tool] --> B["aura.tool_requested"]
    B --> |"Immediate UI feedback (tool_id, tool_name, arguments)"| C
    C --> D["aura.tool_start"]
    D --> |"Has progress_token for correlation"| E["aura.progress"]
    E --> |"MCP server sends updates<br/>(uses progress_token)"| F
    F --> G["aura.tool_complete"]
    G --> |"Final result<br/>(duration_ms, success, result\/error)"| H((" "))
```

### Event Formats

Custom events use the SSE `event:` field to distinguish from standard OpenAI chunks:

**Tool requested** (immediate UI feedback when LLM decides to call a tool):

```
event: aura.tool_requested
data:
```

```json theme={null}
{
  "tool_id": "call_abc123",
  "tool_name": "list_files",
  "arguments": {"path": "/tmp"},
  "agent_id": "main",
  "session_id": "sess_xyz"
}
```

**Tool start** (when MCP execution actually begins):

```
event: aura.tool_start
data:
```

```json theme={null}
{
  "tool_id": "call_abc123",
  "tool_name": "list_files",
  "progress_token": 42,
  "agent_id": "main",
  "session_id": "sess_xyz"
}
```

Note: `progress_token` is included when available from the MCP client. Use it to correlate with `aura.progress` events.

**Tool complete (success)**:

```
event: aura.tool_complete
data:
```

```json theme={null}
{
  "tool_id":"call_abc123",
  "tool_name":"list_files",
  "duration_ms":1234,
  "success":true,
  "result":"file1.txt\nfile2.txt... [truncated]",
  "agent_id":"main",
  "session_id":"sess_xyz"
}
```

**Tool complete (failure)**:

```
event: aura.tool_complete
data:
```

```json theme={null}
{
  "tool_id": "call_abc123",
  "tool_name": "failing_tool",
  "duration_ms": 50,
  "success": false,
  "error": "Tool returned an error: Connection refused",
  "agent_id": "main",
  "session_id": "sess_xyz"
}
```

Note:

* Successful tool results include the `result` field (truncated per `TOOL_RESULT_MAX_LENGTH`, default 1000 chars)
* Tool errors are automatically detected from Rig's error format prefixes (`ToolCallError:`, `JsonError:`, `Tool returned an error:`)
* When detected, `success` is set to `false` and the `error` field contains the full error message

**Reasoning** (requires both flags):

```bash theme={null}
AURA_CUSTOM_EVENTS=true AURA_EMIT_REASONING=true cargo run --bin aura -- webserver
```

```
event: aura.reasoning
data:
```

```json theme={null}
{
  "content": "Let me analyze the request...",
  "agent_id": "main",
  "session_id": "sess_xyz"
}
```

**Progress** (MCP notifications from long-running tools):

```
event: aura.progress
data:
```

```json theme={null}
{
  "message": "Processing step 3 of 5",
  "phase": "mcp_progress",
  "percent": 60,
  "progress_token": 42,
  "agent_id": "main",
  "session_id": "sess_xyz"
}
```

Note: Progress events are only emitted when:

1. `AURA_CUSTOM_EVENTS=true` is set
2. The MCP server sends `notifications/progress` messages during tool execution

**Session info** (emitted once at stream start):

```
event: aura.session_info
data:
```

```json theme={null}
{
  "model": "gpt-5.2",
  "model_context_limit": 200000,
  "session_id": "sess_xyz"
}
```

Note: `aura.session_info` includes only `CorrelationContext` fields (`session_id`, `trace_id`). No `agent_id`. `model_context_limit` comes from the `context_window` field in the `[agent.llm]` TOML config section (or `[orchestration.worker.<name>.llm]` for per-worker overrides). If `context_window` is not set, `model_context_limit` is omitted from the event.

**Skill replay** (emitted once at stream start, after `aura.session_info`, when the turn replays skills recorded on earlier turns of the chat session):

```
event: aura.skills_rehydrated
data:
```

```json theme={null}
{
  "skills": ["code-review", "code-review/references/checklist.md"],
  "session_id": "sess_xyz"
}
```

Note: each `skills` entry names one replayed invocation. A `load_skill` call appears as the skill name, and a `read_skill_file` call appears as `<skill>/<path>`. The list includes only invocations that replayed on this turn, and the event is omitted when nothing replays. `aura.skills_rehydrated` includes only `CorrelationContext` fields (`session_id`, `trace_id`). No `agent_id`. For how AURA records and replays skills, see [Skill Persistence Across Turns](/aura/skills#skill-persistence-across-turns).

**MCP status** (emitted once when at least one MCP server is configured. At stream start in single-agent mode, or just after the shared manager connects in orchestration mode):

```
event: aura.mcp_status
data:
```

```json theme={null}
{
  "servers": [
    {
      "server_name": "mezmo",
      "transport": "http_streamable",
      "status": "connected",
      "tools_count": 7
    },
    {
      "server_name": "pagerduty",
      "transport": "http_streamable",
      "status": "failed",
      "tools_count": 0,
      "reason": "Connection failed: HTTP MCP server 'pagerduty' authentication failed (401 Unauthorized). Check that your headers, forwarded headers. and/or credentials are correct."
    }
  ],
  "session_id": "sess_xyz"
}
```

Note: `status` is one of `connected`, `failed`, or `not_attempted`. This lets a client distinguish a server that is configured but unavailable (`failed`, with a `reason`) from one that connected and legitimately exposes no tools (`connected`, `tools_count: 0`). `reason` is present only for failed servers. The event is omitted entirely when no servers are configured. `aura.mcp_status` includes only `CorrelationContext` fields (`session_id`, `trace_id`) — no `agent_id`.

In orchestration mode all workers share a single `McpManager`, so one `aura.mcp_status` reports the whole run's server status. The wire shape is identical to single-agent mode; it just arrives slightly later (after the manager connects, before planning) rather than at stream start. Requires `AURA_CUSTOM_EVENTS=true` in both modes.

**Worker phase** (phase transitions in multi-agent mode):

```
event: aura.worker_phase
data:
```

```json theme={null}
{
  "phase": "executing",
  "task_id": "task_1",
  "agent_id": "log_worker",
  "parent_agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

Possible `phase` values: `"planning"`, `"executing"`, `"analyzing"`. `task_id` and `parent_agent_id` are omitted when not set.

**Tool usage** (usage snapshot after tool execution rounds):

```
event: aura.tool_usage
data:
```

```json theme={null}
{
  "tool_ids": ["call_abc123", "call_def456"],
  "prompt_tokens": 18777,
  "completion_tokens": 500,
  "total_tokens": 19277,
  "agent_id": "main",
  "session_id": "sess_xyz"
}
```

Emitted from the `on_stream_completion_response_finish` hook when usage data is available. Associates the completed tool IDs with a token usage snapshot. Carries `agent_id` (`main` for single-agent, `coordinator` in orchestration mode), the same agent-context field the other agent-scoped `aura.*` events use, alongside its `CorrelationContext`.

**Usage** (final token usage at stream end):

```
event: aura.usage
data:
```

```json theme={null}
{
  "prompt_tokens": 21500,
  "completion_tokens": 342,
  "total_tokens": 21842,
  "session_id": "sess_xyz"
}
```

Use `prompt_tokens` with `model_context_limit` from `aura.session_info` to calculate context window fill percentage: `(prompt_tokens / model_context_limit) * 100`. No `agent_id` field (only `CorrelationContext`).

**Scratchpad usage** (per-agent report when an agent finishes with scratchpad activity):

```
event: aura.scratchpad_usage
data: {"agent_id":"main","tokens_intercepted":15840,"tokens_extracted":1200,"session_id":"sess_xyz"}
```

Emitted once per agent that used scratchpad. It fires for both single-agent and orchestration worker contexts (in the latter, `agent_id` is the worker name). `tokens_intercepted` is the total tool output diverted to disk; `tokens_extracted` is what the agent pulled back into context via the scratchpad exploration tools.

**Approval requested** (HITL approval request raised):

```
event: aura.approval_requested
data:
```

```json theme={null}
{
  "decision_id": "019edead-beef-7000-8000-000000000001",
  "tool_name": "restart_deployment",
  "tool_namespace": "k8s",
  "origin": {
    "kind": "config_gate",
    "matched_pattern": "restart_*"
  },
  "scope": {
    "kind": "worker",
    "run_id": "019edead-beef-7000-8000-000000000002",
    "task_id": 3,
    "worker": "operations",
    "session_id": "sess_xyz"
  }
}
```

**Approval pending** (conversational HITL is waiting for an attended decision):

```
event: aura.approval_pending
data:
```

```json theme={null}
{
  "decision_id": "019edead-beef-7000-8000-000000000001",
  "tool_name": "restart_deployment",
  "tool_namespace": "k8s",
  "arguments": {"namespace": "prod", "deployment": "api"},
  "origin": {
    "kind": "config_gate",
    "matched_pattern": "restart_*"
  },
  "scope": {
    "kind": "worker",
    "run_id": "019edead-beef-7000-8000-000000000002",
    "task_id": 3,
    "worker": "operations",
    "session_id": "sess_xyz"
  },
  "expires_at": "2026-06-23T22:15:30Z"
}
```

`tool_namespace` is the `[mcp.servers.<key>]` config key of the MCP server that provides the tool. It is omitted for tools that don't come from an MCP server. See [Scope a Pattern to One MCP Server](/aura/configuration-reference#scope-a-pattern-to-one-mcp-server).

**Approval completed** (HITL approval reached a terminal outcome):

```
event: aura.approval_completed
data:
```

```json theme={null}
{
  "decision_id": "019edead-beef-7000-8000-000000000001",
  "outcome": {
    "kind": "denied",
    "reason": "maintenance window not open"
  },
  "duration_ms": 1820,
  "scope": {
    "kind": "worker",
    "run_id": "019edead-beef-7000-8000-000000000002",
    "task_id": 3,
    "worker": "operations",
    "session_id": "sess_xyz"
  }
}
```

Approval events are emitted even when `AURA_CUSTOM_EVENTS=false` because they are
protocol lifecycle events, not optional telemetry. The webhook route emits
`aura.approval_requested` before dispatch and `aura.approval_completed` for all
terminal webhook outcomes. The conversational route emits
`aura.approval_requested`, then `aura.approval_pending` while the tool call is
parked, then `aura.approval_completed` after the decision, timeout, or
cancellation. `outcome.kind` is one of `approved`, `denied`, `timed_out`,
`cancelled`, or `errored`; `errored` represents channel faults such as transport
errors, non-2xx responses, or invalid JSON. `aura.approval_pending` is reserved
for the conversational route and is not emitted by the webhook route.

### Client Handling

Standard OpenAI clients will ignore these events (they only process `data:` lines without `event:` prefix). Custom clients can filter by event type:

```javascript theme={null}
for (const line of chunk.split('\n')) {
  if (line.startsWith('event: ')) {
    const eventType = line.slice(7);
    // Handle aura.tool_start, aura.tool_complete, etc.
  }
  if (line.startsWith('data: ')) {
    const data = JSON.parse(line.slice(6));
    // Handle OpenAI chunk or custom event data
  }
}
```

### Correlation Fields

Most custom events include correlation fields for tracing:

| Field | Description |
| - | - |
| `session_id` | Chat session ID (from request metadata) |
| `trace_id` | OTEL trace ID (when available) |
| `agent_id` | Agent identifier (`main` for single-agent) |

Approval lifecycle events carry `decision_id` and HITL `scope` instead of the shared `AgentContext` / `CorrelationContext` fields. Use `decision_id` to correlate `aura.approval_requested` with `aura.approval_completed`, and inspect `scope` for the requesting surface (`single`, `worker`, or future `coordinator`).

#### Tool Event Correlation

Use these fields to correlate tool-related events:

| Correlation | Events | Field |
| - | - | - |
| Tool lifecycle | `tool_requested` → `tool_start` → `tool_complete` | `tool_id` |
| Progress updates | `tool_start` → `progress` | `progress_token` |

Example correlation:

```mermaid theme={null}
flowchart TD
    A["tool_requested<br/>(tool_id: 'call_abc')"] -->|"shows arguments to user"| B
    B["tool_start<br/>(tool_id: 'call_abc', progress_token: 42)"] -->|"MCP execution begun"| C
    C["progress<br/>(progress_token: 42, progress: 50, total: 100)"] -->|"correlates via token"| D
    D["tool_complete<br/>(tool_id: 'call_abc', duration_ms: 1234)"] -->|"final result"| E((" "))
```

## Orchestration Events

When `orchestration.enabled = true` and `AURA_CUSTOM_EVENTS=true`, the server emits orchestration-specific events covering the Plan/Execute/Continue lifecycle. These events are emitted alongside the standard `aura.*` events above.

### Orchestration Event Types

| Event | Description |
| - | - |
| `aura.orchestrator.plan_created` | Coordinator decomposed query into a task DAG |
| `aura.orchestrator.direct_answer` | Coordinator answered without orchestration |
| `aura.orchestrator.clarification_needed` | Coordinator needs user clarification |
| `aura.orchestrator.task_started` | Worker began executing a task |
| `aura.orchestrator.task_completed` | Worker finished task (success/failure with duration) |
| `aura.orchestrator.worker_reasoning` | Worker reasoning content with task/worker attribution |
| `aura.orchestrator.iteration_complete` | Iteration finished with replan decision, reasoning, and phase timing (planning/execution/tool ms) |
| `aura.orchestrator.replan_started` | Replan cycle triggered (coordinator-routed or task failures) |
| `aura.orchestrator.synthesizing` | Coordinator merging worker results (includes iteration number) |
| `aura.orchestrator.tool_call_started` | A tool call began (coordinator or worker); see the tool-coverage note below |
| `aura.orchestrator.tool_call_completed` | The matching tool call finished (duration, success) |

### Orchestration Event Flow

```mermaid theme={null}
flowchart TD
    A([User query received]) --> R{Coordinator routing}

    R -->|orchestrated / routed| B["plan_created<br/>(goal, tasks, routing_mode, routing_rationale)"]
    R -->|simple query| DA["direct_answer<br/>(response, routing_rationale)"]
    R -->|ambiguous| CL["clarification_needed<br/>(question, options, routing_rationale)"]

    DA --> Z([Done])
    CL --> Z

    B --> C["task_started<br/>Worker assigned (task_id, worker_id, orchestrator_id)"]
    C --> D["worker_reasoning<br/>Worker thinking (task_id, worker_id, content)"]
    D --> E["tool_call_started<br/>Worker calls a tool: MCP, skill, or operation (tool_call_id, tool_name, worker_id)"]
    E --> F["tool_call_completed<br/>Tool result (tool_call_id, worker_id, duration_ms, success)"]
    F --> G["task_completed<br/>Worker finished (duration_ms, success, result)"]
    G --> H["synthesizing<br/>Coordinator merging results (iteration)"]

    H --> PE{Post-execute routing}
    PE -->|respond| DA2["direct_answer<br/>(response, routing_rationale)"]
    PE -->|clarify| CL2["clarification_needed<br/>(question, options, routing_rationale)"]
    PE -->|replan| J["replan_started<br/>trigger: 'post_execute_create_plan'"]

    DA2 --> I["iteration_complete<br/>(will_replan: false)"]
    CL2 --> I
    J --> I2["iteration_complete<br/>(will_replan: true)"]
    I2 -->|loop back| B

    I --> Z
```

**Routing decisions happen twice**: once on initial query (before any work) and again post-execute (after workers finish). Both paths can produce `direct_answer`, `clarification_needed`, or a new plan. The initial routing has no `iteration_complete`; the post-execute routing always emits one.

### Orchestration Event Formats

**Plan created** (coordinator decomposed query into tasks):

```
event: aura.orchestrator.plan_created
data:
```

```json theme={null}
{
  "goal": "Calculate (3+7)*2 and list files",
  "tasks": ["Calculate (3+7)*2", "List files in /tmp"],
  "routing_mode": "orchestrated",
  "routing_rationale": "Multi-step: arithmetic + file listing",
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

The `routing_mode` field indicates how the coordinator routed the query:

* `"routed"`: classified to a single worker
* `"orchestrated"`: multi-task DAG with continuation

The optional `planning_response` field contains the coordinator's raw planning text and is omitted when empty.

**Direct answer** (coordinator answered without orchestration):

```
event: aura.orchestrator.direct_answer
data:
```

```json theme={null}
{
  "response": "The answer is 42",
  "routing_rationale": "Simple factual query, no tools needed",
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

**Clarification needed** (coordinator needs more information):

```
event: aura.orchestrator.clarification_needed
data:
```

```json theme={null}
{
  "question": "Which environment should I check?",
  "options": ["production", "staging", "development"],
  "routing_rationale": "Ambiguous target environment",
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

Note: `options` is omitted when the coordinator does not suggest choices.

**Task started** (worker begins execution):

```
event: aura.orchestrator.task_started
data:
```

```json theme={null}
{
  "task_id": 0,
  "description": "Calculate (3+7)*2",
  "worker_id": "arithmetic",
  "orchestrator_id": "orch-1",
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

**Worker reasoning** (worker thinking with attribution):

```
event: aura.orchestrator.worker_reasoning
data:
```

```json theme={null}
{
  "task_id": 0,
  "worker_id": "arithmetic",
  "content": "I need to add 15 and 27...",
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

Note: requires both `AURA_CUSTOM_EVENTS=true` and `AURA_EMIT_REASONING=true`. Worker reasoning is also emitted as `aura.reasoning` with `agent_id` set to the worker name (e.g., `"arithmetic"`) and `parent_agent_id: "coordinator"` for backward-compatible aggregation.

**Tool call started** (coordinator or worker calls a tool):

```
event: aura.orchestrator.tool_call_started
data:
```

```json theme={null}
{
  "task_id": 0,
  "tool_call_id": "call_abc123",
  "tool_name": "add",
  "worker_id": "arithmetic",
  "arguments": {"a": 3, "b": 7},
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

Note: `task_id` is omitted if it could not be determined. `arguments` is omitted when not available.

**Tool call completed** (the matching tool call finished):

```
event: aura.orchestrator.tool_call_completed
data:
```

```json theme={null}
{
  "task_id": 0,
  "tool_call_id": "call_abc123",
  "worker_id": "arithmetic",
  "success": true,
  "duration_ms": 42,
  "result": "10",
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

Note: `task_id` is omitted if it could not be determined. `result` is truncated per `TOOL_RESULT_MAX_LENGTH` and omitted when empty. `worker_id` names the worker that ran the tool, the same value as on the matching `tool_call_started` event. Older AURA servers omit `worker_id` from this event. To attribute a completion from one of them, match its `tool_call_id` to the `tool_call_started` event.

**Tool coverage:** these events fire for the coordinator (`worker_id: "main"`) as well as workers. They cover MCP tools, the skill tools (`load_skill`, `read_skill_file`), and orchestration operations (`read_artifact`, `submit_result`, `list_prior_runs`). Scratchpad exploration tools are suppressed by default and emit only when `AURA_EMIT_SCRATCHPAD_TOOL_EVENTS` is set.

**Task completed** (worker finished with result):

```
event: aura.orchestrator.task_completed
data:
```

```json theme={null}
{
  "task_id": 0,
  "success": true,
  "duration_ms": 1500,
  "orchestrator_id": "orch-1",
  "worker_id": "arithmetic",
  "result": "The result is 20",
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

**Iteration complete** (replan decision after execution):

```
event: aura.orchestrator.iteration_complete
data:
```

```json theme={null}
{
  "iteration": 1,
  "will_replan": false,
  "reasoning": "All tasks completed successfully",
  "planning_ms": 1180,
  "execution_ms": 4620,
  "task_compute_ms": 4500,
  "tool_ms": 820,
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

The `reasoning` and `gaps` fields are included only when non-empty (i.e., when replanning is triggered).

**Phase timing fields** (all milliseconds, present on every `iteration_complete`):

| Field | Meaning |
| - | - |
| `planning_ms` | Prompt → plan created. Includes planning-correction retries. For replanned iterations this is the prior iteration's continuation-decision latency (that call produced this iteration's plan). |
| `execution_ms` | Plan ready → continuation-prompt entrypoint. One iteration's execution span (worker waves + persistence drain + result consolidation), measured as wall-clock. |
| `task_compute_ms` | Sum of per-task wall durations across the iteration (aggregate compute; exceeds `execution_ms` when tasks run in parallel). |
| `tool_ms` | Sum of tool-call durations recorded for the iteration's tasks. |

To separate time the LLM spent *deciding what to call* from time spent *executing tools*, compute `execution_ms - tool_ms` ≈ LLM-thinking time. This is exact for single-task routes; for parallel waves it is approximate, since `tool_ms` and `task_compute_ms` are summed compute rather than wall-clock — compare them against `execution_ms` to gauge overlap. The same four fields are written to the run manifest (`phase_timings`) and recorded as `orchestration.{planning,execution,task_compute,tool}_ms` attributes on the `orchestration.iteration` OTel span.

**Replan started** (new planning cycle triggered):

```
event: aura.orchestrator.replan_started
data:
```

```json theme={null}
{
  "iteration": 2,
  "trigger": "post_execute_create_plan",
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

Triggers: `"post_execute_create_plan"` (coordinator routed back to planning after evaluating worker results).

**Synthesizing** (consolidating task results for coordinator decision):

```
event: aura.orchestrator.synthesizing
data:
```

```json theme={null}
{
  "iteration": 1,
  "agent_id": "coordinator",
  "session_id": "sess_xyz"
}
```

Fires before the post-execute coordinator call. Bookends with `iteration_complete`, which fires after the coordinator's routing decision.

Note: Workers that use scratchpad emit `aura.scratchpad_usage` when they finish — see the [Custom Event Types](#custom-event-types) section above. This is a base `aura.*` event (not orchestration-specific) so the same event fires for single-agent deployments and workers alike.

### Orchestration Correlation

| Correlation | Events | Field |
| - | - | - |
| Task lifecycle | `task_started` → `worker_reasoning` → `tool_call_*` → `task_completed` | `task_id` |
| Tool lifecycle | `tool_call_started` → `tool_call_completed` | `tool_call_id` |
| Worker identity | `task_started`, `task_completed`, `worker_reasoning`, `tool_call_started`, `tool_call_completed` | `worker_id` |
| Agent hierarchy | All orchestration events | `agent_id` (`"coordinator"` or worker name) |
| Replan cycle | `iteration_complete` → `replan_started` → `plan_created` | `iteration` |

## SSE Event Reference

### Event Types by Mode

| Event | Description | `none` | `open-web-ui` | `aura` |
| - | - | :-: | :-: | :-: |
| **Text chunk** | Token-by-token content | ✅ | ✅ | ✅ |
| **Tool call** | Tool name + arguments | ✅ (with args) | ✅ (empty args) | ✅ (with args) |
| **Tool result** | Tool execution output | - | ✅ (via `tool_calls`) | ✅ (via aura.tool\_complete) |
| **Final chunk** | `finish_reason` + usage | ✅ | ✅ | ✅ |
| **\[DONE]** | Stream termination | ✅ | ✅ | ✅ |

### Message Formats

**First text chunk** (includes `role`):

```json theme={null}
{
  "choices": [
    {
      "delta": {
        "role": "assistant",
        "content": "Hello"
      }
    }
  ]
}
```

**Subsequent text chunks**:

```json theme={null}
{
  "choices": [
    {
      "delta": {
        "content": " world"
      }
    }
  ]
}
```

**Tool call (`none` mode)** - includes actual arguments:

```json theme={null}
{
  "choices": [
    {
      "delta": {
        "tool_calls": [
          {
            "index": 0,
            "id": "call_xyz",
            "type": "function",
            "function": {
              "name": "list_files",
              "arguments": "{\"path\":\"/tmp\"}"
            }
          }
        ]
      }
    }
  ]
}
```

**Tool call (`open-web-ui` mode)** - empty arguments for UI compatibility:

```json theme={null}
{
  "choices": [
    {
      "delta": {
        "tool_calls": [
          {
            "index": 0,
            "id": "call_xyz",
            "type": "function",
            "function": {
              "name": "list_files",
              "arguments": ""
            }
          }
        ]
      }
    }
  ]
}
```

**Tool result (`open-web-ui` mode only)** - sent as second delta with same index:

```json theme={null}
{
  "choices": [
    {
      "delta": {
        "tool_calls": [
          {
            "index": 0,
            "id": "call_xyz",
            "type": "function",
            "function": {
              "name": "",
              "arguments": "{\"files\":[\"a.txt\",\"b.txt\"]}"
            }
          }
        ]
      }
    }
  ]
}
```

**Final chunk**:

```json theme={null}
{
  "choices": [
    {
      "delta": {},
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 10,
    "completion_tokens": 20,
    "total_tokens": 30
  }
}
```

The `usage` object totals every LLM turn in the request. For an orchestrated run, that includes the coordinator's turns and every worker's turns.

**Stream end**:

```
data: [DONE]
```

### `finish_reason` Values

| Value | Meaning |
| - | - |
| `stop` | Normal completion |
| `tool_calls` | Response included tool execution |
| `length` | The response's total `completion_tokens` reached the request's `max_tokens`. In an orchestrated run, that total includes the coordinator and every worker, so a complete answer can report `length`. |

## Client Examples

### JavaScript

```javascript theme={null}
const response = await fetch('/v1/chat/completions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    messages: [{ role: 'user', content: 'List files in /tmp' }],
    stream: true
  })
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  for (const line of decoder.decode(value).split('\n')) {
    if (!line.startsWith('data: ')) continue;
    const data = line.slice(6);
    if (data === '[DONE]') break;

    const chunk = JSON.parse(data);
    const delta = chunk.choices[0]?.delta;

    if (delta?.content) {
      process.stdout.write(delta.content);
    }
    if (delta?.tool_calls) {
      console.log('Tool call:', delta.tool_calls[0].function.name);
    }
  }
}
```

### Python

```python theme={null}
import httpx
import json

with httpx.stream('POST', 'http://localhost:8080/v1/chat/completions',
    json={'messages': [{'role': 'user', 'content': 'Hello!'}], 'stream': True}
) as response:
    for line in response.iter_lines():
        if not line.startswith('data: '): continue
        data = line[6:]
        if data == '[DONE]': break

        chunk = json.loads(data)
        delta = chunk['choices'][0].get('delta', {})

        if content := delta.get('content'):
            print(content, end='', flush=True)
        if tool_calls := delta.get('tool_calls'):
            print(f"\nTool: {tool_calls[0]['function']['name']}")
```

## Multi-Turn Tool Execution

Unlike standard OpenAI API (where tool execution is client-side), this server executes tools server-side and continues streaming. After tool execution completes, text resumes with a `\n\n` separator for readability:

```
I'll check that for you.
[tool call: list_files]
[tool executes server-side]

Here are the files I found:
...
```

The separator is automatically injected when text chunks resume after a `ToolResult` event.

## Connection Behavior

| Behavior | Description |
| - | - |
| **Timeout** | 900s default (configurable via `STREAMING_TIMEOUT_SECS`, 0 = disabled) |
| **Disconnect** | Server detects client disconnect and cancels in-flight operations |
| **Backpressure** | Bounded buffer prevents memory exhaustion |
| **Cancellation** | Timeout or disconnect triggers MCP tool cancellation via `notifications/cancelled` |

## Graceful Shutdown

On SIGTERM or SIGINT, the server stops taking new requests, gives in-flight requests time to finish, and then ends any that remain so their OpenTelemetry spans are exported before it exits:

```mermaid theme={null}
flowchart TD
    A([SIGTERM/SIGINT received]) --> B

    B["**Phase 1: Gate** _(immediate)_
    • New requests rejected with 503
    • In-flight streams continue running"]

    B -->|"all requests finish within the grace period"| E
    B -->|"grace period expires _(SHUTDOWN_TIMEOUT_SECS, default 30s)_"| C

    C["**Phase 2: Drain** _(up to 5s)_
    • Remaining streams cancelled
    • Each stream sends \[DONE\] to client
    • MCP cleanup (cancel + close)"]

    C -->|"all requests finish"| E
    C -->|"5s drain window expires"| D

    D["**Phase 3: Abort**
    • Request tasks still running are aborted"]

    D --> E

    E["**Shutdown complete**
    • Completion logged
    • OpenTelemetry spans flushed _(up to 5s)_"]

    E --> F([Server exits])
```

| Phase | Timing | What happens |
| - | - | - |
| **Gate** | Immediate | Middleware returns 503 for all new requests (including `/health`) |
| **Grace period** | 0 – `SHUTDOWN_TIMEOUT_SECS` (default 30s) | In-flight streams continue running. If they all finish, shutdown skips Drain and Abort. |
| **Drain** | After the grace period, up to 5s | `stream_shutdown_token` cancelled; remaining streams send `[DONE]`, then MCP cleanup runs |
| **Abort** | After the 5s drain window | The server logs `Stream shutdown grace period expired, aborting stragglers` and aborts request tasks that are still running |
| **Exit** | After the last request ends | The server logs `shutdown complete`, flushes OpenTelemetry spans for up to 5s, and exits |

The `shutdown complete` log line carries two fields:

| Field | `true` when |
| - | - |
| `clean_shutdown` | Every in-flight request finished during the grace period or the drain window, so the server aborted nothing |
| `unaborted_tasks` | The abort step didn't finish within 400 ms, so some aborted request tasks hadn't stopped yet |

For the per-stream drain steps and how shutdown differs from a client disconnect, see [Request Lifecycle](/aura/request-lifecycle#shutdown-sequence).

Configure the grace period:

```bash theme={null}
# Allow 60 seconds for in-flight requests to finish
SHUTDOWN_TIMEOUT_SECS=60 cargo run --bin aura -- webserver

# Or via CLI flag
cargo run --bin aura -- webserver --shutdown-timeout-secs 60
```

**K8s tip**: Set `terminationGracePeriodSeconds` to at least `SHUTDOWN_TIMEOUT_SECS + 15` (default: 45s). After the grace period, shutdown can take about 10 more seconds: up to 5s to drain streams and up to 5s to flush OpenTelemetry spans. During Phase 1, `/health` returns 503. Readiness probes will fail immediately, removing the pod from service endpoints.

<Note>The `/health` endpoint returns 503 during shutdown (same middleware gate as all routes). This is intentional — it signals load balancers and K8s readiness probes to stop routing traffic to this instance.</Note>

## Response Headers

```http theme={null}
Content-Type: text/event-stream
Cache-Control: no-cache
X-Accel-Buffering: no
X-Chat-Session-Id: sess_xyz
```

`X-Chat-Session-Id` carries the request's chat session ID. Send it back on later turns to continue the same session. For how AURA picks the ID, see [How AURA Identifies a Conversation](/aura/skills#how-aura-identifies-a-conversation).
