7. Reasoning items are canonical and replayed; summary text stays ephemeral
Copy Markdown View SourceDate: 2026-05-29 Status: Accepted
Context
ADR 0004 classified reasoning as ephemeral (reasoning_delta, never persisted)
and froze the canonical set to user_message, assistant_message, tool_call, tool_result, permission_decision. That conflated two different things that share the
word "reasoning":
- the reasoning summary text — human-facing, streamed for live display; and
- the encrypted reasoning item (
rs_…, type"reasoning", carryingencrypted_content) that the OpenAI Responses API emits on a tool turn and requires re-injected asinputon subsequent turns, ahead of its pairedfunction_call(fc_…).
Because Pixir is stateless (store: false, ADR 0003) and folds the Log into input
every Turn, an rs_ that is never persisted is never threaded back. Today
Pixir.Provider requests include: ["reasoning.encrypted_content"] but discards the
item, keeping only ephemeral deltas. On long multi-tool turns this is the biggest
correctness risk (ROADMAP §C item 1): the model loses the reasoning chain it paid to
produce. The official docs are explicit that, with store: false, reasoning is carried
across turns only by re-sending these encrypted items (with include on every
request) and keeping them in order between the last user message and the tool outputs.
Decision
The encrypted reasoning item is canonical; the summary text stays ephemeral. This refines ADR 0004 rather than overturning it.
New canonical event
reasoning— a sibling oftool_call, not a field onassistant_message/tool_call(a pure-tool turn has no finalassistant_messageto hang it on, and it maps 1:1 to the wire item per ADR 0004's thesis).data: %{"item" => <raw SSE reasoning object, opaque>, "model" => <capturing model id>}. The item — includingencrypted_contentand itsrs_id — is stored verbatim and never interpreted (mirrors Pi'sJSON.stringify(item)), so server-side schema drift round-trips.reasoning_deltaremains ephemeral for live display.Ordering is owned by the Turn loop, via
seq. The Provider captures output items in arrival order (a single ordered stream of reasoning items + function calls, alongside the existing flatreasoning_items/function_callslists). The Turn loop recordsreasoningandtool_callevents in that order, so monotonicseqguarantees everyrs_precedes itsfc_and preserves any intra-turn interleaving — no extra plumbing, and the deferred "exact interleaving" question becomes a non-issue.Replay (
to_input_item/1) re-injects the item verbatim, guarded by model. Areasoningevent whose stored"model"differs from the current request model is dropped — an encrypted item is only valid for the model that produced it (mirrors Pi'sisDifferentModel). Dropping anrs_is always safe; replaying a stale one risks a 400. Pixir already sends nofc_ids on tool calls (onlycall_id), so the companion "null thefc_id when itsrs_is dropped" step Pi needs is satisfied for free.include: ["reasoning.encrypted_content"]is re-sent on every request (already the case).
Consequences
- Log schema change — exactly the deliberate curation ADR 0004 named ("adding a
canonical type is a schema change to the Log"). Old logs (no
reasoningevents) fold unchanged; new logs carry them. - Drop, don't replay, on these conditions: model mismatch (above); reasoning from errored / iteration-capped turns (a reasoning item with no following item is rejected — "reasoning without following item").
- Not guarded:
encrypted_contentstaleness acrossresume/time. The docs state no expiry and treat encrypted content as the cross-turn mechanism; we add no speculative time-based drop. If the backend ever rejects a stale item, that is an empirical finding to handle inclassify_http_errorthen. - Open follow-ups: whether the API's
rs_/fc_pairing check is positional or strictly id-based is unconfirmed (Pi relies on order + omitted ids); resolve via the live-verify harness if a batched replay is ever rejected. Strict per-pairfc_ids remain a deferred refinement.