docs(notes): remove duplicate promoted system-prompt proposal
The implemented owner at .agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md retains the decision, alternatives, consequences, and verification. Remove only the reintroduced pre-promotion triplet under the lifecycle rules in .agents/notes/README.md; the dependent feature proposal already links to the implemented owner. No proposal archival or body rewrite is needed.
This commit is contained in:
1 parent
cfc10f7e1f
commit
449fb9c71e
3 files changed
-162
No files matched your search
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-09-02-system-prompt-as-surface-node.md
|
||||
2026-09-02-system-prompt-as-surface-node.md: 56577a7199235e95f4a7c6500140c8a8841d48dc
|
||||
2026-09-02-system-prompt-as-surface-node.zh.md: da864fd300c93cae0210e758562b823c0a61679a
|
||||
@@ -1,78 +0,0 @@
|
||||
# Agent Note: The system prompt is surface node 0
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-02-system-prompt-as-surface-node.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The system prompt has a different durable representation from every other message the model reads. Conversation messages are surface events (`user/message`, `assistant/message`, `tool/result`) folded in seq order by `Session.deriveMessages()`; the system prompt is the `system` field of the log-only `request/header` snapshot, and each DeepSeek serializer prepends it as wire message 0 (`serializeRequest`, `serializeRequestWithImages`). The [reconstructable-requests Agent Note](../../implemented/architecture/2026-07-05-reconstructable-requests.md) made both halves durable, but it left one model-visible fact with two homes: the surface owns the messages, the header owns the message in front of them.
|
||||
|
||||
That split forces every reader of "what did the model see" to join two sources. The compaction summarizer (`buildSummarizationInput`) copies `header.system` in front of the region's derived messages; `dsh-token-meter` estimates the system prompt from the header while pricing every other message from the surface; the Web request-prompt card, the trajectory view, and the snapshot normalizer's `{{system}}` placeholder each read the header on their own. The loop's change detection is also split: `headerEquals` compares `system` byte-for-byte beside `config` and `tools`, so a prompt change and a tool change are indistinguishable in the log (`request/header` reason `change`) even though they are different operations on the conversation.
|
||||
|
||||
The split also blocks the next step. A model that accepts a mid-conversation `system` message as a prompt replacement needs the harness to append a system-role message to history; with the prompt living in the header there is no surface representation to append, and the header would have to be frozen by special case. The [in-history replacement proposal](../feature/2026-09-02-in-history-system-prompt-replacement.md) depends on this note.
|
||||
|
||||
## Proposal
|
||||
|
||||
Move the system prompt onto the surface. It becomes an ordinary surface event, `system/message`, and every prompt lifecycle operation is one of the two existing `SurfaceOp` variants applied to that event type. The wire request does not change: the surface fold yields the same message list the serializers already build today, with the system message first.
|
||||
|
||||
### The event
|
||||
|
||||
`system/message` joins `SurfaceEventType` beside `user/message`, `assistant/message`, and `tool/result`. Its payload mirrors `tool/result`: `{ turn, step, message }`, where `message` is a `Message` with `role: 'system'`, exactly one text block holding the rendered prompt, and source `{ kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' }`. `deriveEventMessage` projects it verbatim, so `deriveMessages()` returns the system message at its surface position and both DeepSeek serializers, which already pass a `role: 'system'` history message through unchanged, emit it as wire message 0. `EpochHeader.system` is removed; the header keeps `config`, `adapterDefaults`, and `tools`.
|
||||
|
||||
### The operations
|
||||
|
||||
| Situation | Surface operation |
|
||||
|---|---|
|
||||
| First request of a session with a non-empty rendered prompt | append `system/message` as surface node 0, before the first `user/message` of the step |
|
||||
| Rendered prompt differs from the prompt at node 0 | replace node 0: `surfaceOp: { op: 'replace', start: <seq of node 0>, end: <same> }`, `sourceEventSeqs: [<seq of node 0>]` |
|
||||
| Rendered prompt is empty on the first request | no system node; a later non-empty prompt appends node 0 when the surface has no system node yet |
|
||||
|
||||
Replacing node 0 is today's head rewrite expressed on the surface: the provider prefix changes from the first token, the log records the shadowed node through `sourceEventSeqs`, and `replaceGeneration` advances exactly as it does for a compaction replacement, so the loop's existing `startsSeries` detection (`requestSurfaceGeneration !== surfaceGeneration`) covers the prompt change without a `system` comparison in `headerEquals`. `request/header` keeps reasons `initial`, `resume`, `change`, and `series`; `change` now means config or tools changed.
|
||||
|
||||
### Ownership in the loop
|
||||
|
||||
`dsh-agent-loop` owns a `SystemPromptProjection` beside `RuntimeContextProjection` in `runtime-context.ts`. It restores the current system node from the log (the latest surviving `system/message` on the surface), follows `session/event` for new system nodes and for replacements whose `sourceEventSeqs` shadow the retained one, and returns the uncommitted append or replace intent when the rendered prompt differs. `turn()` commits that intent immediately before the step's `user/message` events, so the log order is the wire order. `step()` no longer passes `system` to `buildRequest`; the request is `header.config` plus `deriveMessages()` plus `header.tools`. The `dsh-agent-loop/invariant` companion keeps comparing the rebuilt request against the frozen one, now with the system message inside `messages`. `docs/architecture.md` records the new loop step order: claim, assemble, project system prompt, project runtime context, pre-step, commit system node, commit user messages, build request.
|
||||
|
||||
### Consumers retargeted
|
||||
|
||||
| Consumer | Today | After |
|
||||
|---|---|---|
|
||||
| DeepSeek serializers (`serializeRequest`, `serializeRequestWithImages`) | prepend `options.system` | serialize `options.messages` only; `GenerateOptions.system` remains for direct one-shot callers such as the summarizer and title providers |
|
||||
| `compaction-basic` `buildSummarizationInput` | `header.system` + region messages | node 0's derived message + region messages, still a genuine prefix of the routed request |
|
||||
| `compaction-basic` `selectCompactableRange` | head-anchored at `surfaceNodes[0]` | anchored at the first non-system node; node 0 is never inside a compaction range |
|
||||
| `dsh-token-meter` system estimate | `header.system` length | the system node is priced like every other surface node; the context breakdown labels it by its source plugin |
|
||||
| Web request-prompt card, trajectory request-header node, request inspection | read `header.system` | read the `system/message` node; the card keeps its collapsed inspectable presentation and is never a chat bubble |
|
||||
| Snapshot normalizer `{{system}}` placeholder, plan-mode tests asserting `header.system` | header | the system node's text |
|
||||
| TypeScript and Python SDK expected outputs | no system event | include the `system/message` event |
|
||||
| Human transcript projections (`isAppendSurfaceEvent` readers) | no system events | skip `system/message`; it is model history, not conversation |
|
||||
|
||||
`RuntimeContextProjection` and `SystemPromptProjection` are symmetric: both watch owned surface nodes and their shadowing through `sourceEventSeqs`, and both hand the loop an uncommitted message that `turn()` commits. The difference is the role and the operation set — runtime context appends user-role snapshots only, the system prompt appends once and then replaces.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep `header.system` and add `system/message` only for updates.** Two homes for one fact: every consumer above would read the header for message 0 and the surface for later messages, and the loop would need a special case that ignores `system` in `headerEquals` while a surface system node exists. Rejected because the point of the change is one representation.
|
||||
|
||||
**A dedicated log-only `system-prompt/change` event that rewrites the header.** Preserves the header as the home of the prompt and records changes as their own event kind, but still cannot express a system message inside history, so the in-history proposal would need a second mechanism anyway. Rejected.
|
||||
|
||||
**Synthesize the system message inside the adapter from consecutive headers.** The adapter is stateless per request and never sees the log; a wire history that depends on adapter state is not reconstructable from the surface fold. Rejected.
|
||||
|
||||
**Express the prompt as a `user/message` snapshot like runtime context.** Reuses an existing event type but sends the wrong role, so a model that treats a system message as authoritative would not. Rejected.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `SurfaceEventType` contains `system/message`; `deriveEventMessage` projects it; `Session.append('system/message', …)` requires a `SurfaceIntent` like the other surface events.
|
||||
- `EpochHeader` has no `system` field; `headerEquals` compares `config`, `adapterDefaults`, and `tools` only.
|
||||
- A first request with a non-empty rendered prompt appends `system/message` as surface node 0 before the step's first `user/message`; a changed prompt replaces node 0 with `sourceEventSeqs` naming the shadowed node; an unchanged prompt appends nothing.
|
||||
- The DeepSeek wire request for every loop step is byte-identical to today's for the same session history: system first, then the folded conversation.
|
||||
- Compaction never selects node 0; the summarizer's replayed prefix starts with node 0's derived message.
|
||||
- `dsh-token-meter`, the Web request-prompt card, trajectory and inspection views, the snapshot normalizer, plan-mode tests, and both SDK expected outputs read the system node; the `dsh-agent-loop/invariant` companion rebuilds requests with the system message inside `messages`.
|
||||
- Keyless recorded snapshots that exercise a mid-session prompt change (plan mode entering and leaving) show a replaced node 0 instead of a `request/header` `change`.
|
||||
- `docs/architecture.md`, the `dsh-agent-loop`, `dsh-session`, `dsh-system-prompt`, `dsh-compaction-basic`, and `dsh-token-meter` READMEs, and the reconstructable-requests Agent Note describe the surface node as the home of the system prompt.
|
||||
|
||||
## Risks
|
||||
|
||||
- Every reader of `header.system` moves in one change; a missed reader fails at compile time because the field is gone, which is the intended failure mode.
|
||||
- Compaction region selection gains an invariant (node 0 is never compacted). A compaction provider other than `compaction-basic` that anchors at `surfaceNodes[0]` would shadow the prompt; the `dsh-session` surface manager rejects a replacement whose range covers surface node 0 while node 0 is a `system/message` unless the replacing event is itself a `system/message` covering exactly that node, so the invariant is enforced where the operation happens, not only in the shipped provider. System nodes at later positions carry no such protection: a compaction range may shadow them.
|
||||
- Replacing node 0 advances `replaceGeneration`, which today means "compaction happened" to some readers; those readers switch to inspecting the replacement event's type.
|
||||
- Recorded snapshot fixtures whose logs contain `header.system` are re-recorded; the fixtures, not the normalizer, change.
|
||||
@@ -1,78 +0,0 @@
|
||||
# Agent Note: 系统提示词是 surface 的第 0 号节点
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-02-system-prompt-as-surface-node.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
系统提示词的持久化表示与模型读到的其他所有消息都不同。对话消息是 surface 事件(`user/message`、`assistant/message`、`tool/result`),由 `Session.deriveMessages()` 按 seq 顺序折叠;系统提示词则是仅记日志的 `request/header` 快照中的 `system` 字段,每个 DeepSeek 序列化器把它前置为协议消息 0(`serializeRequest`、`serializeRequestWithImages`)。[可重建请求 Agent Note](../../implemented/architecture/2026-07-05-reconstructable-requests.zh.md) 让两半都成为持久数据,却让一个模型可见的事实拥有两个归属:surface 拥有消息,header 拥有排在这些消息之前的那条消息。
|
||||
|
||||
这种拆分迫使每个想知道「模型看到了什么」的读取方都要合并两个来源。压缩摘要器(`buildSummarizationInput`)把 `header.system` 复制到区域派生消息之前;`dsh-token-meter` 从 header 估算系统提示词,却从 surface 为其他每条消息计价;Web 请求提示词卡片、轨迹视图和快照归一化器的 `{{system}}` 占位符各自单独读取 header。循环的变更检测同样被拆开:`headerEquals` 在 `config` 和 `tools` 旁边逐字节比较 `system`,因此提示词变更与工具变更在日志中无法区分(`request/header` 的 reason 都是 `change`),尽管它们是对对话的两种不同操作。
|
||||
|
||||
这种拆分还阻塞了下一步。一个把对话中途的 `system` 消息当作提示词替换来接受的模型,需要 harness 向历史追加一条 system 角色消息;当提示词住在 header 里时,没有可追加的 surface 表示,header 也只能靠特例被冻结。[历史内替换提案](../feature/2026-09-02-in-history-system-prompt-replacement.zh.md) 依赖本 Agent Note。
|
||||
|
||||
## Proposal
|
||||
|
||||
把系统提示词搬到 surface 上。它成为一个普通的 surface 事件 `system/message`,提示词生命周期中的每个操作都是对该事件类型施加现有两种 `SurfaceOp` 变体之一。协议请求不变:surface 折叠产出的消息列表与序列化器今天构建的完全相同,系统消息在最前面。
|
||||
|
||||
### 事件
|
||||
|
||||
`system/message` 加入 `SurfaceEventType`,与 `user/message`、`assistant/message`、`tool/result` 并列。它的载荷与 `tool/result` 对称:`{ turn, step, message }`,其中 `message` 是 `role: 'system'` 的 `Message`,恰好一个文本块承载渲染后的提示词,source 为 `{ kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' }`。`deriveEventMessage` 逐字投影它,因此 `deriveMessages()` 在其 surface 位置返回系统消息,而两个 DeepSeek 序列化器本已原样透传 `role: 'system'` 的历史消息,会把它作为协议消息 0 发出。`EpochHeader.system` 被移除;header 保留 `config`、`adapterDefaults` 和 `tools`。
|
||||
|
||||
### 操作
|
||||
|
||||
| 情形 | surface 操作 |
|
||||
|---|---|
|
||||
| 会话首个请求且渲染后的提示词非空 | 追加 `system/message` 作为 surface 第 0 号节点,位于该步骤首条 `user/message` 之前 |
|
||||
| 渲染后的提示词与第 0 号节点不同 | 替换第 0 号节点:`surfaceOp: { op: 'replace', start: <第 0 号节点的 seq>, end: <同一值> }`,`sourceEventSeqs: [<第 0 号节点的 seq>]` |
|
||||
| 首个请求时渲染后的提示词为空 | 没有系统节点;之后出现非空提示词且 surface 尚无系统节点时,追加为第 0 号节点 |
|
||||
|
||||
替换第 0 号节点就是今天的头部重写在 surface 上的表达:提供方前缀从第一个 token 起改变,日志通过 `sourceEventSeqs` 记录被遮蔽的节点,`replaceGeneration` 与压缩替换时一样推进,因此循环现有的 `startsSeries` 检测(`requestSurfaceGeneration !== surfaceGeneration`)无需在 `headerEquals` 中比较 `system` 即可覆盖提示词变更。`request/header` 保留 `initial`、`resume`、`change`、`series` 四种 reason;`change` 现在表示 config 或 tools 变更。
|
||||
|
||||
### 循环中的归属
|
||||
|
||||
`dsh-agent-loop` 在 `runtime-context.ts` 中与 `RuntimeContextProjection` 并列拥有一个 `SystemPromptProjection`。它从日志恢复当前系统节点(surface 上最新存活的 `system/message`),跟随 `session/event` 观察新的系统节点以及 `sourceEventSeqs` 遮蔽了所保留节点的替换,并在渲染后的提示词不同时返回未提交的追加或替换意图。`turn()` 紧接在该步骤的 `user/message` 事件之前提交该意图,因此日志顺序即协议顺序。`step()` 不再向 `buildRequest` 传递 `system`;请求由 `header.config`、`deriveMessages()` 和 `header.tools` 构成。`dsh-agent-loop/invariant` 伴随组件继续把重建的请求与冻结的请求比较,只是系统消息现在位于 `messages` 内。`docs/architecture.md` 记录新的循环步骤顺序:领取、装配、投影系统提示词、投影运行时上下文、pre-step、提交系统节点、提交用户消息、构建请求。
|
||||
|
||||
### 消费方迁移
|
||||
|
||||
| 消费方 | 现状 | 变更后 |
|
||||
|---|---|---|
|
||||
| DeepSeek 序列化器(`serializeRequest`、`serializeRequestWithImages`) | 前置 `options.system` | 只序列化 `options.messages`;`GenerateOptions.system` 为摘要器、标题提供方等直接单次调用方保留 |
|
||||
| `compaction-basic` 的 `buildSummarizationInput` | `header.system` + 区域消息 | 第 0 号节点的派生消息 + 区域消息,仍是已路由请求的真实前缀 |
|
||||
| `compaction-basic` 的 `selectCompactableRange` | 锚定在头部 `surfaceNodes[0]` | 锚定在首个非系统节点;第 0 号节点永不落入压缩范围 |
|
||||
| `dsh-token-meter` 的系统提示词估算 | `header.system` 长度 | 系统节点与其他每个 surface 节点一样计价;上下文明细按其 source 插件标注 |
|
||||
| Web 请求提示词卡片、轨迹请求 header 节点、请求检视 | 读取 `header.system` | 读取 `system/message` 节点;卡片保持折叠可检视的呈现,永不作为聊天气泡 |
|
||||
| 快照归一化器的 `{{system}}` 占位符、断言 `header.system` 的 plan-mode 测试 | header | 系统节点的文本 |
|
||||
| TypeScript 与 Python SDK 期望输出 | 没有系统事件 | 包含 `system/message` 事件 |
|
||||
| 人类转录投影(`isAppendSurfaceEvent` 的读取方) | 没有系统事件 | 跳过 `system/message`;它是模型历史,不是对话 |
|
||||
|
||||
`RuntimeContextProjection` 与 `SystemPromptProjection` 是对称的:两者都通过 `sourceEventSeqs` 观察自己拥有的 surface 节点及其被遮蔽的情况,都把一条未提交的消息交给循环由 `turn()` 提交。区别在于角色与操作集——运行时上下文只追加 user 角色快照,系统提示词追加一次之后只做替换。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留 `header.system`,只为更新添加 `system/message`。** 一个事实两个归属:上述每个消费方都要从 header 读消息 0、从 surface 读后续消息,循环还需要一个在 surface 存在系统节点时让 `headerEquals` 忽略 `system` 的特例。被否决,因为本次变更的目的就是单一表示。
|
||||
|
||||
**用专门的仅记日志事件 `system-prompt/change` 重写 header。** 保留 header 作为提示词归属,并把变更记录为独立事件种类,但仍无法表达历史内部的系统消息,历史内替换提案还是需要第二套机制。被否决。
|
||||
|
||||
**在适配器内根据相邻 header 合成系统消息。** 适配器逐请求无状态且从不接触日志;依赖适配器状态的协议历史无法从 surface 折叠重建。被否决。
|
||||
|
||||
**像运行时上下文那样用 `user/message` 快照表达提示词。** 复用了现有事件类型,却发送了错误的角色,因此把系统消息视为权威的模型不会这样对待它。被否决。
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `SurfaceEventType` 包含 `system/message`;`deriveEventMessage` 投影它;`Session.append('system/message', …)` 与其他 surface 事件一样要求 `SurfaceIntent`。
|
||||
- `EpochHeader` 没有 `system` 字段;`headerEquals` 只比较 `config`、`adapterDefaults` 和 `tools`。
|
||||
- 渲染后的提示词非空的首个请求在该步骤首条 `user/message` 之前追加 `system/message` 作为 surface 第 0 号节点;提示词变更时以指明被遮蔽节点的 `sourceEventSeqs` 替换第 0 号节点;提示词不变时不追加任何内容。
|
||||
- 对同一会话历史,每个循环步骤的 DeepSeek 协议请求与今天逐字节一致:系统消息在先,随后是折叠后的对话。
|
||||
- 压缩永不选中第 0 号节点;摘要器回放的前缀以第 0 号节点的派生消息开头。
|
||||
- `dsh-token-meter`、Web 请求提示词卡片、轨迹与检视视图、快照归一化器、plan-mode 测试以及两个 SDK 的期望输出都读取系统节点;`dsh-agent-loop/invariant` 伴随组件重建请求时系统消息位于 `messages` 内。
|
||||
- 演练会话中途提示词变更(进入与退出 plan 模式)的无密钥录制快照显示被替换的第 0 号节点,而不是 `request/header` 的 `change`。
|
||||
- `docs/architecture.md`、`dsh-agent-loop`、`dsh-session`、`dsh-system-prompt`、`dsh-compaction-basic`、`dsh-token-meter` 的 README 以及可重建请求 Agent Note 都把 surface 节点描述为系统提示词的归属。
|
||||
|
||||
## Risks
|
||||
|
||||
- `header.system` 的每个读取方在一次变更中迁移;遗漏的读取方因字段消失而在编译期失败,这正是预期的失败方式。
|
||||
- 压缩范围选择新增一条不变量(第 0 号节点永不被压缩)。除 `compaction-basic` 以外、锚定在 `surfaceNodes[0]` 的压缩提供方会遮蔽提示词;`dsh-session` 的 surface 管理器拒绝在第 0 号节点是 `system/message` 时覆盖第 0 号节点的替换,除非替换事件本身是恰好覆盖该节点的 `system/message`,因此不变量在操作发生处被强制,而不只在随发的提供方中。位于更后位置的系统节点没有此类保护:压缩范围可以遮蔽它们。
|
||||
- 替换第 0 号节点会推进 `replaceGeneration`,今天有些读取方把它理解为「发生了压缩」;这些读取方改为检查替换事件的类型。
|
||||
- 日志中包含 `header.system` 的录制快照 fixture 需要重新录制;改变的是 fixture,而不是归一化器。
|
||||
Reference in new issue
Block a user