---
title: Message Flow & Ordering
description: How messages travel between Human, engine, LLM, Environment and Trace — every ordering guarantee and non-guarantee, and why stream order differs from context order.
---

[The OmniMessage Protocol](/omni-message) defines what messages *are*; this page explains how they *move* and in what order they become visible: the delivery paths, the merge mechanism, the observable timeline within a turn, which orderings are guaranteed, which are not, and why "order on the stream" and "order in the model context" are two different things. Source of truth: `packages/core/src/engine/context-engine.ts`.

## Delivery paths within a turn

Five actors: Human (the SDK caller), engine (context_engine), LLM, Environment, Trace. Within one turn:

```text
Human ──run(newMessages)──► engine
                            engine ──write Prompt──────────────────► Trace
                            engine ──request_begin──► Human and Trace
                            engine ──streamGenerate(new messages)──► LLM
        ┌────────────  LLM streams partial_* and complete messages ────────┐
        │  engine forwards each: simultaneously ──► Human (yield)          │
        │                                      and ──► Trace (write)       │
        └──────────────────────────────────────────────────────────────────┘
   complete tool_call ──► engine: await approve(tc) (one at a time)
                            engine ──approval_decision──► Human and Trace
                 allow ──► Environment.executeTool (concurrent, never blocks the LLM stream)
        Environment ──partial_tool_call_output──► Human, and (complete) ──► Trace
   LLM stream ends: token_usage is its last message, request_end follows at once
   still-running tools keep streaming output (possibly after request_end)
   all outputs settled ──► reordered to original call order as the next turn's LLM input
```

Key point: **every message is written to the Trace at the same moment it enters the output stream**, so stream order and Trace order agree (the Trace merely skips partials and `origin`-tagged messages — see [Sessions & Traces](/sessions-and-traces)).

## The merge point: MergeQueue

A turn has several concurrent producers: the driver task consuming the LLM stream, plus N concurrently executing tools. All of them push into one merge queue, and a **single consumer** (the `run` generator) yields messages one at a time in **arrival order**; the turn ends only when every producer has finished and the queue is drained.

This one mechanism fixes three basic properties of message delivery:

1. the consumer sees a single **totally ordered** stream — no client-side multiplexing needed;
2. messages from different producers interleave by arrival time — tool outputs arrive in **completion order**, unrelated to call order;
3. order *within* one producer is preserved (the LLM stream is internally ordered; a single tool's fragments are ordered).

## The observable order within a turn

A turn with two tool calls, as the consumer observes it (annotated):

```text
 1   event     request_begin
 2   partial   partial_thinking(start → delta… → stop)
 3   complete  thinking                       ← the complete message right after stop
 4   partial   partial_text(start → delta… → stop)
 5   complete  text
 6   partial   partial_tool_call A(start → delta… → stop)
 7   complete  tool_call A
 8   event     approval_decision(allow, A)    ← approvals are sequential; A starts executing
 9   partial   partial_tool_call B(…)         ← the LLM stream continues, not waiting for A
10   complete  tool_call B
11   event     approval_decision(allow, B)
12   partial   partial_tool_call_output B(…)  ← B produces output first: completion order
13   complete  tool_call_output B
14   event     token_usage                    ← the LLM stream's last message
15   event     request_end(completed)         ← emitted when the LLM stream ends, not waiting for tools
16   partial   partial_tool_call_output A(…)  ← late output lands after request_end
17   complete  tool_call_output A
     (A and B settled → re-fed in A, B original order → next request_begin)
```

If a `tool_call` is denied, line 8 carries `deny` and a synthetic `aborted` `tool_call_output` ("Tool call denied by user.") follows immediately — nothing is dispatched.

## Guarantees and non-guarantees

**Guaranteed:**

| Guarantee | Meaning |
| --- | --- |
| Streaming discipline | every segment goes strictly `start → delta* → stop`, complete message right after; concatenated deltas ≡ the complete message |
| Approval position | `approval_decision` comes after its `tool_call` and before any output of that tool |
| Pairing | every committed `tool_call` gets exactly one complete `tool_call_output` (a denial gets the synthetic one) |
| LLM stream tail | `token_usage` is the LLM stream's last message, `request_end` follows immediately |
| Commit criterion | `request_end.status === "completed"` ⇔ the turn was committed by the gateway (replay keeps or drops on this) |
| Stream order = Trace order | written as streamed; the Trace only filters partials and `origin` messages |
| Transport ordering | SSE delivers per channel with monotonic ids; reconnects replay from `Last-Event-ID` or get `resync_required` — see [Server API](/server-api) |

**Not guaranteed (renderers must not rely on these):**

| Non-guarantee | Meaning |
| --- | --- |
| Tool-output order | arrival is completion order; fragments of different tools interleave — attribute by `tool_call_id` |
| `request_end` ≠ end of turn | still-running tools may emit output after `request_end` and before the next `request_begin` |
| Event/content spacing | later LLM-stream messages may land between an `approval_decision` and that tool's first output |

## Stream order vs context order

The same batch of tool outputs exists in two orders, serving two different consumers:

- **stream order (completion order)** — for the Human: whoever finishes first is visible first, for real-time rendering;
- **context order (original call order)** — for the model: before entering the next turn's input, outputs are reordered to the original `tool_call` order, matching provider pairing rules.

Therefore **a renderer must never reconstruct the context from arrival order** — hang each output onto its call via `tool_call_id`; the engine owns context ordering.

## Edge-case timelines

| Case | Observable order on the stream |
| --- | --- |
| User interrupt | (messages produced so far) → the `abort` event — the last message before `run` returns; carry-over goes to the model context only, never streamed, never written to Trace |
| Automatic reconnect | `request_end(failed \| timeout \| malformed)` → a fresh `request_begin` (up to 5 times, exponential backoff with a 30s ceiling; only `auth` skips the ladder); the `[turn_retried]` block is model-visible only |
| Compaction | `compaction_begin` → the compaction request runs against the old context (its streamed output is **not** forwarded, only written to Trace) → that request's `token_usage` → `compaction_end(status)` |
| max_turns reached | a length notice → the run ends; unsubmitted input is kept as carry-over |
| The Prompt itself | written to Trace, not echoed back onto the stream (the caller already has it) |
| session_meta | never emitted on the main Session's stream (it lives in the Trace and the history API); a Subagent child stream's **first** message is the child's `session_meta` |

## Across Sessions: the origin chain

A child Session spawned by `run_subagent` has its own complete stream. When forwarded to the parent, each child message gets one child-Session-id hop prepended to `origin`, and it interleaves with the parent's own messages **by arrival time**; renderers route by `origin` into the nested card. Child messages are not written to the parent Trace — the parent keeps only the `subagent` pointer event, while the child's stream order is recorded in its own Trace.

## Transport ordering (SSE)

The Server pushes this exact output stream verbatim (single-line JSON) onto the per-Session SSE channel: monotonically increasing event ids, a bounded replay buffer for reconnects, `resync_required` when the replay window is gone. Event order: on reconnect the replayed gap (or `resync_required`) comes first, then the authoritative `task_state` snapshot and pending approvals; a fresh connection skips replay, so `task_state` is its first event. Details — including the bundled Web App's connect-first + dedup consumption pattern — are on the [Server API](/server-api) page.
