docs: replace vague provenance prose with recorded facts

This commit is contained in:
Turtle
2026-08-09 15:35:02 +08:00
parent 8c124f84b6
commit 9704749b01
380 changed files with 946 additions and 874 deletions
@@ -8,13 +8,13 @@ English | [中文](2026-07-28-identified-immutable-message-values.zh.md)
The harness had several message-shaped representations with different identity rules. Agent input acquired an inbox correlation id only when the loop accepted it, while durable user messages, assistant messages, tool results, and model-request messages could have no identity. Prompt admission therefore sat between creation and identity, and equivalent content was copied across live events, durable events, and model requests without one value that named the message throughout its lifetime.
This made identity a routing side effect rather than a message invariant. Producers could not refer to a message before calling the agent, prompt hooks received content and source separately, and later projections had to reconstruct a message while deciding whether an id existed. Immutability also began at different boundaries: some inputs were frozen by the loop, some only by session append, and provider-produced assistant output used a separate provenance-bearing shape.
This made identity a routing side effect rather than a message invariant. Producers could not refer to a message before calling the agent, prompt hooks received content and source separately, and later projections had to reconstruct a message while deciding whether an id existed. Immutability also began at different boundaries: some inputs were frozen by the loop, some only by session append, and provider-produced assistant output used a separate shape carrying provider, model, and replay state.
## Decision
`@deepseek-ai/dsh-llm` owns one `Message` value with required `id`, `role`, `content`, and `source`. `MessageId` is opaque and shared by user, assistant, and tool-result messages. A message receives its id at creation, before inbox routing, claim, pre-step rewriting, durable append, or request projection. The same id survives every representation boundary.
`createMessage(input)` is the canonical role-generic creation boundary. It mints a `MessageId`, detaches the supplied role, content, and source, and deep-freezes the complete value before returning it. `createUserMessage({ content, source })` fixes the user role for prompt and context producers. `createAssistantMessage({ content, source })` fixes both the assistant role and the model source kind, so model-output producers supply only content and model provenance. All creation helpers exclude an input id so callers cannot accidentally present creation as import. `freezeMessage(message)` is the separate import or transformation boundary: it detaches and deep-freezes a message whose identity already exists, without minting a replacement.
`createMessage(input)` is the canonical role-generic creation boundary. It mints a `MessageId`, detaches the supplied role, content, and source, and deep-freezes the complete value before returning it. `createUserMessage({ content, source })` fixes the user role for prompt and context producers. `createAssistantMessage({ content, source })` fixes both the assistant role and the model source kind, so model-output producers supply only content plus provider, model, and optional replay state. All creation helpers exclude an input id so callers cannot accidentally present creation as import. `freezeMessage(message)` is the separate import or transformation boundary: it detaches and deep-freezes a message whose identity already exists, without minting a replacement.
The helpers live in `dsh-llm` beside the base message vocabulary because their complete contracts depend only on that vocabulary. `createToolResultMessage()` belongs with the other creation helpers: it couples a tool call id to the exact user-role tool-result block and source without depending on session state or events. `dsh-session` consumes complete messages rather than owning their construction.
@@ -40,7 +40,7 @@ Every message producer must choose creation or import explicitly, and tests cons
Live inbox events, durable events, derived history, and model requests can correlate one message without content equality or envelope-specific ids. Pending-input policy and UI attachment cleanup can compare `MessageId` before a turn exists, while claims retain that identity inside the open turn. Deep freezing prevents a producer, hook, or observer from changing the value after identity is established.
The shared representation removes the old `UserMessageData`/`AgentMessage` split and folds provider provenance into typed message sources. Event envelopes still own facts that are not message semantics, such as turn and step position, token usage, internal tool failure identity, and presentation metadata.
The shared representation removes the old `UserMessageData`/`AgentMessage` split and puts provider, model, and optional replay state into typed message sources. Event envelopes still own facts that are not message semantics, such as turn and step position, token usage, internal tool failure identity, and presentation metadata.
The message and helper unit tests pin immediate identity, detachment, deep immutability, and preservation of an imported id. Agent-loop tests pin identity across admission, inbox lifecycle, durable append, content rewriting, and cancellation; session tests pin frozen derivation and identity-preserving replacement.