replay: never turn thinking into speech
This commit is contained in:
parent
59b52a818b
commit
707a277273
2 changed files with 167 additions and 64 deletions
|
|
@ -8,10 +8,9 @@ The code is `core::session::replay_messages`.
|
|||
|
||||
## One history
|
||||
|
||||
There is a single record: the messages stored for a conversation. Everything
|
||||
that talks to a model — her turn, her subconscious a moment later, a terminal,
|
||||
a voice, a web surface — replays that same record. None of them holds a version
|
||||
of its own.
|
||||
There is a single record: typed message blocks stored for a conversation. Her
|
||||
turn, her subconscious a moment later, the TUI, and the Panel all read that
|
||||
record. None of them holds a transcript of its own.
|
||||
|
||||
That is not tidiness. It follows from what the substrate already says: one
|
||||
consciousness in two modes, and she is not the glass. A second projection is a
|
||||
|
|
@ -19,50 +18,92 @@ second opinion about what happened, and the two drift the moment one is
|
|||
touched. There were three of them once. They disagreed about images before
|
||||
anyone noticed.
|
||||
|
||||
## What crosses
|
||||
One record does not mean one lossy encoding. A surface receives the blocks
|
||||
verbatim and draws their types. A model provider receives the narrowest valid
|
||||
wire projection of those blocks. Both are views of the same record; neither is
|
||||
allowed to rewrite the record into its own authority.
|
||||
|
||||
A stored message is made of blocks. Each one has a prose form for replay:
|
||||
## What crosses to a model
|
||||
|
||||
| block | crosses as |
|
||||
A stored message is made of blocks. The model projection preserves the
|
||||
strongest valid form each block still has:
|
||||
|
||||
| stored shape | crosses as |
|
||||
|---|---|
|
||||
| text | itself |
|
||||
| tool use | `Tool use: name(input)` |
|
||||
| tool result | `Result (name): output`, or `Error (name): output` |
|
||||
| reasoning | `[Reasoning]: …` |
|
||||
| image | natively, or a marker saying it was not seen |
|
||||
| text | assistant/user text |
|
||||
| complete tool round | one assistant `tool_calls` message, then bound tool results |
|
||||
| call with no result | native call plus an explicit did-not-complete result |
|
||||
| result whose call is gone | `Result (name): output`, or `Error (name): output` prose |
|
||||
| reasoning | omitted unless a provider-valid signed thinking block exists |
|
||||
| image | native image, or a marker saying it was not seen |
|
||||
|
||||
Nothing is silently dropped. A block with no prose form does not exist — the
|
||||
match is exhaustive, so a new kind of block fails to compile rather than
|
||||
quietly going missing.
|
||||
Text crosses as speech. Tool calls and their results cross as a native tool
|
||||
round when the pair exists; orphaned evidence degrades explicitly rather than
|
||||
breaking the wire. Images cross natively when the model can see and otherwise
|
||||
become an honest unseen marker.
|
||||
|
||||
This is what makes a turn's own work available to the turn after it. When tool
|
||||
blocks were dropped on replay, everything she learned by *doing* survived only
|
||||
as whatever she happened to write down in prose, and she would re-derive
|
||||
Reasoning is different. It remains a typed `reasoning` block in persistence,
|
||||
token accounting, compaction input, transcript APIs, and the Panel's
|
||||
collapsible Thinking card. It is not assistant speech. Anthropic will only
|
||||
accept replayed thinking with the original signature, which the generic
|
||||
persisted block does not carry. The honest generic projection therefore omits
|
||||
it. Flattening it into `[Reasoning]: ...` puts an internal trace into the
|
||||
assistant's visible prose and teaches the next model to imitate the leak.
|
||||
|
||||
The projection matches exhaustively. Adding a block kind still fails review at
|
||||
the owning matches rather than disappearing behind a wildcard.
|
||||
|
||||
Native tool rounds are what make a turn's own work available to the turn after
|
||||
it. When tool blocks were dropped, everything she learned by *doing* survived
|
||||
only as whatever she happened to write down in prose, and she would re-derive
|
||||
findings she had already reached. It also left microcompact — whose entire job
|
||||
is blurring old tool output — with nothing in the payload to blur.
|
||||
is blurring old tool output — with nothing in the stored payload to blur.
|
||||
|
||||
## Two properties that are load-bearing
|
||||
Microcompact does not drop tools. It retains every call and replaces only the
|
||||
bulk of older tool-result output with an explicit cleared marker, keeping the
|
||||
recent results intact. The typed record is compacted first; replay projects the
|
||||
result afterward.
|
||||
|
||||
Both were learned from a failure, and both look like details until they break.
|
||||
## Three properties that are load-bearing
|
||||
|
||||
**One wire message per stored message.** Replaying block by block splits a
|
||||
single assistant turn that called several tools into adjacent assistant
|
||||
messages, and OpenAI-shaped providers reject that outright: *assistant
|
||||
tool_calls must be followed by tool messages*. The message boundary is part of
|
||||
the record.
|
||||
All three were learned from failures, and all look like details until they
|
||||
break.
|
||||
|
||||
**No tool roles and no call-id obligations.** Inside a live turn, a tool call
|
||||
and its result are both known to exist, and the exact linkage is used. Across
|
||||
persisted turns that linkage is brittle: a turn killed mid-round leaves a call
|
||||
whose result never landed, and replaying it as half a pair fails the whole
|
||||
request. So the *content* of a tool round survives as prose while the wire
|
||||
obligations do not. The record is not the protocol.
|
||||
**One declaration for one stored assistant round.** Replaying block by block
|
||||
splits a turn that called several tools into adjacent assistant messages.
|
||||
OpenAI-shaped providers reject that shape. All calls from the stored assistant
|
||||
message remain on one wire message.
|
||||
|
||||
## The only thing that varies
|
||||
**A native call is always answered.** Each result follows the assistant tool
|
||||
declaration with its original call id. If a turn died before a result landed,
|
||||
replay synthesizes an explicit did-not-complete result. If compaction removed
|
||||
the call but left its result, that result cannot carry a tool role honestly and
|
||||
degrades to named success/error prose. No half-pair reaches a provider.
|
||||
|
||||
Whether the model can see. Images ride as native multipart content, or degrade
|
||||
to a marker that says plainly they were not visible. The caller supplies that
|
||||
one fact; nothing else about the projection changes.
|
||||
**Doing never becomes saying.** A tool call must not become assistant prose
|
||||
such as `Tool use: bash(...)`. That falsifies the record and teaches the model
|
||||
that writing the sentence is how the action is performed. Calls stay calls;
|
||||
results stay bound evidence whenever the pair can still be made valid.
|
||||
|
||||
## Surface hydration
|
||||
|
||||
`GET /v1/conversations/:id/messages` returns the stored
|
||||
`ConversationMessage`s verbatim. The Panel maps `text` to speech, `reasoning`
|
||||
to `ThinkingCard`, and tool use/result blocks to `ToolCard`. Live SSE follows
|
||||
the same vocabulary: `assistant_message`, `reasoning_message`, tool call, and
|
||||
tool return remain distinct events. A resume must use this typed path, never
|
||||
`replay_messages`, which is the model projection.
|
||||
|
||||
That separation makes the visible invariant precise: Reasoning and Thinking
|
||||
are the same typed thing. They may be folded or hidden by a surface, and kept
|
||||
for later audit or reflection, but they may not be relabelled as ordinary text.
|
||||
|
||||
## The model capability that varies
|
||||
|
||||
Whether the model can see changes the image projection. Images ride as native
|
||||
multipart content, or degrade to a marker that says plainly they were not
|
||||
visible. The caller supplies that one fact; nothing else about the generic
|
||||
projection changes.
|
||||
|
||||
A degraded image is named as unseen rather than dropped. A model told it did
|
||||
not look can say so. A model handed a stub shaped like success proceeds as
|
||||
|
|
@ -70,6 +111,12 @@ though it had looked.
|
|||
|
||||
## Open edges
|
||||
|
||||
Live tool rounds inside a turn build their own messages, with real
|
||||
`tool_call_id` linkage. That is correct and separate — replay is for what is
|
||||
already on disk. The two should not be merged; they answer different questions.
|
||||
Live tool rounds inside a turn and replayed completed rounds both use real
|
||||
`tool_call_id` linkage. Their repair rules differ: live execution knows whether
|
||||
a result is still coming, while replay must make an interrupted persisted round
|
||||
valid and truthful before it crosses the wire.
|
||||
|
||||
Persisting provider-valid signed thinking would permit a native reasoning
|
||||
projection for providers that support it. Until the record carries that proof,
|
||||
reasoning stays available to surfaces and later internal readers but does not
|
||||
cross as invented assistant speech.
|
||||
|
|
|
|||
Loading…
Reference in a new issue