Project injected content verbatim, dropping the XML envelopes
注入内容逐字投影,去除 XML 封套
Two families of injected session content rendered into the model transcript wrapped in XML envelopes: `steering/message` as `<steering source="…">…</steering>` and `context/message` as `<context source="…">…</context>` (the latter with a `'raw'` opt-out that skipped the wrapper). The envelopes aimed to tell the model "this is injected, not the user speaking." Two problems: - **No model is trained on these tags.** `<s
English
Problem
Two families of injected session content rendered into the model transcript wrapped in XML envelopes: steering/message as <steering source="…">…</steering> and context/message as <context source="…">…</context> (the latter with a 'raw' opt-out that skipped the wrapper). The envelopes aimed to tell the model "this is injected, not the user speaking."
Two problems:
- No model is trained on these tags.
<steering>and<context>are arbitrary markup no model was taught to read, so the framing adds tokens without a reliable effect and can actively mislead — recorded transcripts show a model treating a<steering>instruction as third-party metadata and refusing it while answering only the original prompt. - The session surface is the wrong layer for framing. The surface projects the durable log into the model transcript; deciding how content is worded is not its job. A caller that wants a particular frame formats its own content before injecting it — which the one heavy producer (
agent-instructions) already does, owning its complete<system-reminder>frame and opting out of the<context>wrapper withenvelope: 'raw'. The remaining tag machinery (ContextEnvelope, anenvelopefield threaded throughInjectOptions,HookContext, thecontext/messageevent, and the loop) served a distinction that belongs to the caller.
Decision
Injected session content projects verbatim; the caller owns any framing. deriveEventMessage renders user/message content blocks to the model unchanged; source stays in the durable event log but does not render.
The ContextEnvelope type and every envelope field are removed — context/message in SessionEventMap, InjectOptions, HookContext, and the inject()/additionalContexts plumbing in dsh-agent-loop. agent-instructions no longer requests 'raw'; its self-framed content renders as before. The renderTagged/renderContextEnvelope helpers are deleted. context/message.meta still carries durable, model-hidden JSON state.
The source attribution the envelopes carried is not lost — it remains on the durable events; it simply no longer renders into the transcript.
Alternatives considered
- Keep the
<context>envelope, unwrap only steering — leaves theContextEnvelope/envelopemachinery alive for a framing bit no model reads, and keeps the inconsistency that the main producer already opts out of. - Keep the envelope field for plugin-sourced content only — splits one projection into two on
source.kindfor no observed benefit; a plugin steering the agent (hook-bridge continuation reasons) also wants the instruction followed, not labeled. - Move the unwrapping into adapters — the canonical projection is the model-visible contract ("model-visible ⟺ logged"); per-adapter divergence on framing would make the derived transcript adapter-dependent. Framing that a caller genuinely wants belongs in the caller's content, not in an adapter.
Consequences
- Mid-turn steering and injected context reach the model with the same weight as an ordinary user prompt.
- The transcript no longer distinguishes injected content from a user message; consumers that need the distinction read the durable event log, which keeps the event types,
source, andmetaintact. - The
hook-{cc,codex}-stop-continueACP snapshots were re-recorded: the old recordings captured the model refusing steering as third-party metadata, the fix's exact failure mode. - The content-block-vocabulary Agent Note's tagged-envelope clause is amended to point here.
Deferred
agent-instructions already frames its own content: it emits a complete <system-reminder>…</system-reminder> block as the message content instead of leaning on a surface-level wrapper. That caller-owned pattern is the one to keep — the surface passes content through verbatim, and any framing lives in the producer's own content.
Two framing paths existed — caller-baked framing (agent-instructions's <system-reminder>) and surface-level wrapping (<context>/<steering> added by deriveEventMessage). This change removes the second, leaving only caller-owned framing. If labeled framing is wanted again, unify it through the event's meta map — the producer-attached, model-hidden metadata field — consumed by a dedicated renderer or adapter, rather than re-hardcoding a tag in deriveEventMessage. A producer declares the frame it wants in meta; one renderer applies it; the session-surface projection stays a verbatim pass-through.
中文
问题
两类注入的会话内容在渲染进模型 transcript(文本记录)时被包在 XML 封套里:steering/message 包成 <steering source="…">…</steering>,context/message 包成 <context source="…">…</context>(后者有一个 'raw' 退出选项可跳过封套)。这些封套意在告诉模型「这是注入内容,不是用户在说话」。
两个问题:
- 没有模型在这些标签上训练过。
<steering>和<context>是任何模型都未被教会去读的任意标记,因此这层框架只是徒增 token 而没有可靠效果,还可能起反作用——已录制的 transcript 显示,模型会把<steering>指令当成第三方元数据而拒绝服从,只回答原始提示词。 - 会话表层是承载框架的错误层次。 表层的职责是把持久日志投影为模型 transcript;决定内容如何措辞并不是它的事。想要特定框架的调用方可以在注入前自行格式化内容——唯一的重度生产方(
agent-instructions)本就这样做,它自带完整的<system-reminder>框架,并用envelope: 'raw'退出<context>封套。剩下的标签机制(ContextEnvelope类型,以及贯穿InjectOptions、HookContext、context/message事件和 agent loop(智能体循环)的envelope字段)所服务的区分,本应归属调用方。
决策
注入的会话内容逐字投影,框架由调用方自行负责。deriveEventMessage 把 user/message 的内容块原样送达模型;source 保留在持久事件日志中,但不渲染。
ContextEnvelope 类型和所有 envelope 字段都被移除——包括 SessionEventMap 中的 context/message、InjectOptions、HookContext,以及 dsh-agent-loop 中 inject()/additionalContexts 的相关管线。agent-instructions 不再请求 'raw';它自带框架的内容渲染方式不变。renderTagged/renderContextEnvelope 辅助函数被删除。context/message.meta 仍携带持久的、对模型隐藏的 JSON 状态。
封套曾携带的 source 来源信息并未丢失——它仍保留在持久事件上;只是不再渲染进 transcript。
权衡的替代方案
- 保留
<context>封套,只对 steering(中途引导)去封套 —— 会为一个没有模型会读的框架位保留ContextEnvelope/envelope机制,并保留主要生产方本就退出的那种不一致。 - 仅对插件来源的内容保留 envelope 字段 —— 会按
source.kind把一条投影拆成两条,却没有观察到任何收益;插件引导 agent 时(钩子桥接器的轮次续行原因)同样希望指令被遵从,而不是被贴标签。 - 把去封套的逻辑移入适配器 —— 规范投影就是模型可见约定(「模型可见 ⟺ 已记录」);让各适配器在框架上各行其是,会使派生的 transcript 依赖于适配器。调用方确实想要的框架应放进调用方自己的内容里,而不是适配器。
结果
- 中途引导与注入的上下文以与普通用户提示词相同的权重到达模型。
- transcript 不再区分注入内容与用户消息;需要这一区分的消费方读取持久事件日志,其中事件类型、
source和meta完整保留。 hook-{cc,codex}-stop-continueACP(Agent Client Protocol)快照已重新录制:旧录制捕获的是模型把 steering 当作第三方元数据而拒绝服从,正是本次修复针对的失败模式。- 内容块词汇表 Agent Note 中关于带标签封套的条款已修订为指向本文。
推迟事项
agent-instructions 已经自行为内容加框架:它把一个完整的 <system-reminder>…</system-reminder> 块作为消息内容发出,而不依赖表层封套。这种调用方自有的模式才是应保留的——表层逐字透传内容,任何框架都住在生产方自己的内容里。
曾经存在两条框架路径——调用方自行加框架(agent-instructions 的 <system-reminder>),以及表层封套(deriveEventMessage 加上的 <context>/<steering>)。本次变更移除了后者,只留下调用方自有的框架。如果未来又需要带标签的框架,应由事件的 meta map(生产方附加、对模型隐藏的元数据字段)来统一它,交给专门的渲染器或适配器消费,而不是在 deriveEventMessage 中重新硬编码标签。生产方在 meta 中声明所需的框架,由一个渲染器统一施加;会话表层的投影始终保持逐字透传。