Files
deepseek-harness/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.zh.md
T
_Kerman 02efe3ccea Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification
# Conflicts:
#	.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml
#	.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md
#	.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml
#	.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml
#	.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md
#	.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.i18n.yaml
#	.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml
#	.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml
#	.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md
#	.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml
#	.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.zh.md
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md
#	.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md
#	.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
#	.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md
#	.agents/notes/implemented/architecture/2026-07-24-separate-context-injection-from-turn-execution.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md
#	.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md
#	.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.zh.md
#	.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.i18n.yaml
#	.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.zh.md
#	.agents/notes/implemented/feature/2026-06-24-workspace-context.i18n.yaml
#	.agents/notes/implemented/feature/2026-06-24-workspace-context.zh.md
#	.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml
#	.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.i18n.yaml
#	.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.zh.md
#	.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml
#	.agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md
#	.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
#	.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md
#	.agents/notes/implemented/feature/2026-07-16-harness-level-loop.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-16-harness-level-loop.zh.md
#	.agents/notes/implemented/feature/2026-07-19-human-goal-command.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md
#	.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.zh.md
#	.agents/notes/implemented/feature/2026-07-21-cross-session-references.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md
#	.agents/notes/implemented/feature/2026-07-27-tmux-location-context.i18n.yaml
#	.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md
#	.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.zh.md
#	.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-27-request-error-retry-action.i18n.yaml
#	docs/core-data-structures/compaction.i18n.yaml
#	docs/core-data-structures/goal.i18n.yaml
#	docs/core-data-structures/goal.zh.md
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/core-data-structures/session.i18n.yaml
#	docs/core-data-structures/session.zh.md
#	docs/core-data-structures/skills.i18n.yaml
#	docs/core-data-structures/skills.zh.md
#	docs/core-data-structures/system-prompt.i18n.yaml
#	docs/defensive-patterns.i18n.yaml
#	docs/defensive-patterns.zh.md
#	docs/user/develop/framework/events.i18n.yaml
#	docs/user/develop/framework/events.zh.md
#	packages/acp/acp/README.i18n.yaml
#	packages/client/ui-goal/README.i18n.yaml
#	packages/compact/compact-basic/README.i18n.yaml
#	packages/compact/compact/README.i18n.yaml
#	packages/context/time-context/README.i18n.yaml
#	packages/context/tmux-context/README.i18n.yaml
#	packages/core/agent-loop/README.i18n.yaml
#	packages/core/agent-loop/README.zh.md
#	packages/core/agent/README.i18n.yaml
#	packages/core/agent/README.zh.md
#	packages/core/session/README.i18n.yaml
#	packages/core/session/README.zh.md
#	packages/core/system-prompt/README.i18n.yaml
#	packages/core/system-prompt/README.zh.md
#	packages/examples/cli-demo/README.i18n.yaml
#	packages/goal/command-goal/README.i18n.yaml
#	packages/goal/goal-session/README.i18n.yaml
#	packages/goal/goal/README.i18n.yaml
#	packages/goal/tool-goal/README.i18n.yaml
#	packages/goal/tool-goal/README.zh.md
#	packages/guard/README.i18n.yaml
#	packages/guard/README.zh.md
#	packages/guard/repeat-tool-guard/README.i18n.yaml
#	packages/hooks/hooks-claude/README.i18n.yaml
#	packages/hooks/hooks-codex/README.i18n.yaml
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/README.zh.md
#	packages/llm/llm/README.i18n.yaml
#	packages/plan/plan-mode/README.i18n.yaml
#	packages/plan/plan-mode/README.zh.md
#	packages/sdk/sdk-client/README.i18n.yaml
#	packages/sdk/sdk-client/README.zh.md
#	packages/sdk/sdk-protocol/README.i18n.yaml
#	packages/session-persistence/session-persistence/README.i18n.yaml
#	packages/session-persistence/session-persistence/README.zh.md
#	packages/skill/tool-skill/README.i18n.yaml
#	packages/subagent/subagent-dsh-sdk/README.i18n.yaml
#	packages/subagent/subagent-inprocess/README.i18n.yaml
#	packages/subagent/subagent-inprocess/README.zh.md
#	packages/ui/jsonrpc/README.i18n.yaml
2026-08-05 13:09:53 +08:00

6.4 KiB
Raw Blame History

Agent Note: 将每条消息创建为带标识的不可变值

Status: implemented

English | 中文

问题

harness 曾存在多种形似消息的表示,各自采用不同的标识规则。agent(智能体)输入只有在 agent loop 接受后才会取得 inbox 关联 id,而持久用户消息、assistant 消息、工具结果和模型请求消息都可能没有标识。因此,提示词准入介于创建消息与建立标识之间;等价内容会在实时事件、持久事件和模型请求之间复制,却没有一个值能在消息的整个生命周期中标识它。

这使标识成为路由的副作用,而不是消息不变量。生产方无法在调用 agent 前引用一条消息,提示词钩子会分别接收内容和来源,后续投影则必须一边重建消息,一边决定 id 是否存在。不可变性也从不同边界开始:部分输入由 agent loop 冻结,部分直到会话追加时才冻结,由提供方生成的 assistant 输出则使用另一种携带溯源信息的形状。

决策

@deepseek-ai/dsh-llm 持有唯一一种 Message 值,其 idrolecontentsource 均为必填。MessageId 是不透明标识,由用户消息、assistant 消息和工具结果消息共享。消息在创建时就会获得 id,早于 inbox 路由、领取、pre-step 改写、持久追加或请求投影。同一个 id 会跨越每个表示边界。

createMessage(input) 是角色通用的规范创建边界。它会生成 MessageId,将传入的角色、内容和来源与调用方对象解除引用关系,并在返回完整值前将其深度冻结。createUserMessage({ content, source }) 为提示词和上下文生产方固定 user 角色。createAssistantMessage({ content, source }) 同时固定 assistant 角色与模型来源类别,因此模型输出生产方只需提供内容和模型溯源信息。所有创建辅助函数的输入都不包含 id,因此调用方不会意外地把新消息的创建伪装成已有消息的导入。freezeMessage(message) 是独立的导入或转换边界:它会将已有标识的消息与调用方对象解除引用关系并深度冻结,不会生成替代标识。

这些辅助函数位于基础消息词汇旁的 dsh-llm 中,因为它们的完整契约只依赖该词汇。createToolResultMessage() 与其他创建辅助函数同属此处:它使用同一个工具调用 id,将工具来源与确切的 user-role 工具结果块耦合起来,不依赖会话状态或事件。dsh-session 只消费完整消息,不负责构造它们。

Agent 接口通过 followupsteerinject 接收完整的 UserMessage。这些操作绝不会分配或返回标识;它们会冻结导入的值,而调用方已经持有该值的 id。inbox 领取和 agent/pre-step 会直接接收该消息。改写内容时会创建具有相同 id 的冻结替代值,而每个附加上下文都是单独创建的 UserMessage,拥有自己的 id。

产生持久消息的事件会存储完整消息。user/message 直接存储其 UserMessageassistant/messagetool/result 则将各自角色专用的消息与事件本地的位置、用量、失败或呈现事实包装在一起。会话派生会返回这些冻结值,而不是重建匿名消息。assistant 组装会在响应完成时创建模型来源的消息,工具执行会在提交结果时创建工具来源的消息。

仅改变已有语义消息表示的操作会保留其 id,并返回另一个冻结值。创建新语义消息的操作则会生成新 id。因此,压缩(compaction)中的内容改写会保留被改写工具结果的标识,而摘要检查点是一条新消息。

考虑过的替代方案

让基础消息的 id 保持可选。 这能减少 fixture(测试前置数据)迁移,并允许提供方或持久化形状继续保持匿名,但也会保留原有歧义:每个消费方都必须根据标识是否存在执行分支,且没有任何类型能证明准入、记录或投影保留了标识。

让 agent 交付分配 id。 这会将标识限定在 inbox 关联范围内,却也会让 agent 调用成为生产方可以标识自身消息的最早时机。这样一来,在交付返回前,提示词构造、UI 附件和同步入队/丢弃协调都需要进行内容匹配,或使用带外 token。

让每个持久事件分配新 id。 这能为持久消息提供标识,却会有意切断它与实时输入的关联,并让回放请求表现得像包含了不同消息。标识属于语义值,而不是承载它的每个封装。

只在 agent 或会话准入时冻结。 这能省去创建辅助函数,却会留下一个带标识但可变的时间区间,调用方代码可以在这段时间内改变该 id 所关联的含义。本决策让「拥有 id」与「是不可变快照」同时成立。

后果

每个消息生产方都必须显式选择创建或导入,测试也会构造完整值,而不是不完整的内容/来源记录。UUID 的生成会前移至最初的语义创建点,因此提供已有 id 的确定性 fixture 会使用 freezeMessage(),而不是 createMessage()

实时 inbox 事件、持久事件、派生历史和模型请求可以关联同一条消息,无需比较内容或使用封装专用 id。待处理输入策略和 UI 附件清理可以在轮次存在之前比较 MessageId,领取后则会在已打开的轮次内保留该标识。深度冻结可以防止生产方、钩子或观察方在标识建立后更改消息值。

共享表示移除了旧的 UserMessageData/AgentMessage 划分,并将提供方溯源信息纳入带类型的消息来源。事件封装仍持有不属于消息语义的事实,例如轮次与步骤位置、token 用量、内部工具失败标识和呈现元数据。

消息和辅助函数的单元测试会锁定即时标识、解除输入引用、深度不可变性,以及导入 id 的保留。agent loop 测试会锁定标识跨越准入、inbox 生命周期、持久追加、内容改写和取消的行为;会话测试会锁定冻结派生和保留标识的替换行为。

相关