Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.zh.md
#	.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml
#	.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-06-sandbox.md
#	.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml
#	docs/architecture.i18n.yaml
#	docs/cookbook/adding-a-tool.i18n.yaml
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/core-data-structures/session.i18n.yaml
#	docs/core-data-structures/tools.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/permission-switching/session.jsonl
#	examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl
#	examples/acp-agent/tests/snapshots/plan-mode/session.jsonl
#	examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl
#	packages/context/session-reference/README.md
#	packages/core/agent-loop/tests/agent.spec.ts
#	packages/hooks/hooks-claude/tests/coverage-cases.ts
#	packages/host/runtime/tests/host-runtime.spec.ts
#	packages/llm/llm-retry/tests/retry.spec.ts
#	packages/session-persistence/session-persistence/src/coordinator.ts
#	packages/support/acp-snapshot/README.md
#	packages/support/acp-snapshot/src/normalize.ts
#	packages/ui/acp/acp-feature-support.md
#	packages/ui/acp/src/codec.ts
#	packages/ui/acp/src/index.ts
#	packages/ui/acp/tests/bridge.spec.ts
#	packages/ui/acp/tests/codec.spec.ts
#	packages/ui/acp/tests/config-options.spec.ts
#	packages/ui/acp/tests/dispose.spec.ts
#	packages/ui/acp/tests/edges.spec.ts
#	packages/ui/acp/tests/stream-update.spec.ts
#	packages/ui/acp/tests/turns.spec.ts
This commit is contained in:
_Kerman
2026-07-26 14:05:33 +08:00
1062 changed files with 33621 additions and 30750 deletions

View File

@@ -2,5 +2,5 @@
# 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
2026-06-19-drop-mutable-session-summary.md: 80fe043e365352b17d2a5b3efa1ab8d396d311c4
2026-06-19-drop-mutable-session-summary.zh.md: e875d96d003367dbe8a671e6057a6b4a6a44ca74
2026-06-19-drop-mutable-session-summary.md: f87378a1c3737950eb536be8e3f6776586eb8a01
2026-06-19-drop-mutable-session-summary.zh.md: 05b6711b71ef87602b46706cd4340a10453e66ad

View File

@@ -12,8 +12,8 @@ The summary was designed for a future session picker (recency ordering via `upda
- `SessionPersistence.update()` has **zero production callers** (every `.update(` hit is `createHash().update()` or a test).
- `firstPrompt` is **never read** anywhere in production.
- `title` *is* read in the ACP bridge — but from a tool-call **presenter** (`present.title`), never from stored session metadata.
- `updatedAt` has **no consumer**: the only production caller of `list()` reads `meta.cwd` (a `SessionHeader` field) to validate a workspace on `session/load`; resume reads `createdAt`/`cwd`/`parentSession` — all header fields.
- Session titles come from durable `session/title` events, while tool-card titles come from tool presenters; neither reads mutable session metadata.
- Persistence-list consumers use immutable header identity, creation, lineage, and cwd fields. Recency and previews derive from the log rather than an `updatedAt` summary.
- Decisively: the live `Session.header` was already typed `SessionHeader`, not `SessionMeta` — the summary never existed on the live session object; it lived only in the persistence layer, written and read by nothing but its own contract test.
## Decision

View File

@@ -12,8 +12,8 @@ Status: implemented
- `SessionPersistence.update()` **零个生产调用方**(所有 `.update(` 匹配都是 `createHash().update()` 或测试代码)。
- `firstPrompt` 在生产代码中**从未被读取**。
- `title` *确实*在 ACP(Agent Client Protocol)桥接层被读取过,但读的是工具调用的 **presenter**(`present.title`),从未读取存储的会话元数据。
- `updatedAt` **没有消费方**:`list()` 唯一的生产调用方读取的是 `meta.cwd`(`SessionHeader` 字段),用于在 `session/load` 时校验工作区;恢复会话读取的是 `createdAt`/`cwd`/`parentSession`——全是 header 字段。
- 会话标题来自持久的 `session/title` 事件,工具卡片标题来自工具 presenter;二者都不读取可变的会话元数据。
- 持久化列表的消费方使用不可变 header 中的标识、创建、谱系和 cwd 字段。近期排序和预览派生自日志,而非某个 `updatedAt` 摘要。
- 决定性的一点:活跃的 `Session.header` 类型本来就是 `SessionHeader` 而非 `SessionMeta`——摘要从未存在于活跃会话对象上;它只存在于持久化层,除了自身的契约测试外无人写入、无人读取。
## 决策

View File

@@ -2,5 +2,5 @@
# 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
2026-06-20-collapse-trace-only-session-events.md: fce5c48ef6fcb1abc6e2fbb95dc7e83d22956660
2026-06-20-collapse-trace-only-session-events.zh.md: 5be2d77644c7b4084b892013a2c1e5b97a333d74
2026-06-20-collapse-trace-only-session-events.md: c77062c3cd44286b43c175702c35b36f9cc31da6
2026-06-20-collapse-trace-only-session-events.zh.md: b232b3fb60822e60b1f5767066db42b0228269a8

View File

@@ -6,7 +6,7 @@ English | [中文](2026-06-20-collapse-trace-only-session-events.zh.md)
## Problem
The session event vocabulary includes first-class events that are not part of replayable conversation history and have little or no production consumption. `usage` is already present as a model stream chunk before the loop also appends a separate `usage` event. `error` duplicates the `turn/end { kind: 'error', message, code }` reason for loop failures; ACP settlement reads the turn-end reason, ACP rendering ignores the `error` event, and `deriveMessages()` skips it.
The session event vocabulary includes first-class events that are not part of replayable conversation history and have little or no production consumption. `usage` is already present as a model stream chunk before the loop also appends a separate `usage` event. `error` duplicates the `turn/end { kind: 'error', message, code }` reason for loop failures; ACP settlement reads the turn-end reason, while message and UI projections skip the standalone `error` event.
These events make the canonical transcript look more useful as telemetry than it currently is. They add event variants, invariants, tests, snapshots, and persistence cases, but they are not load-bearing as separate records. The facts they carry can still be useful: token usage should remain available for accounting, and an error's step number should not silently disappear. The simplification is to fold those facts into nearby events consumers already must understand, not to record less information.

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
会话事件词汇中包含一些一等事件,它们不属于可回放的对话历史,在生产环境中几乎没有消费方。`usage` 已经作为模型流分片存在,之后循环又追加了一个独立的 `usage` 事件。`error` 与 `turn/end { kind: 'error', message, code }` 中的循环失败原因重复;ACP(Agent Client Protocol)结算读取轮次结束原因,ACP 渲染忽略 `error` 事件,`deriveMessages()` 也跳过它。
会话事件词汇中包含一些一等事件,它们不属于可回放的对话历史,在生产环境中几乎没有消费方。`usage` 已经作为模型流分片存在,之后循环又追加了一个独立的 `usage` 事件。`error` 与 `turn/end { kind: 'error', message, code }` 中的循环失败原因重复;ACP(Agent Client Protocol)结算读取轮次结束原因,而消息投影和 UI 投影都会跳过独立的 `error` 事件。
这些事件让规范的 transcript(文本记录)看起来比实际更像遥测数据。它们增加了事件变体、不变式、测试、快照和持久化用例,但作为独立记录并不承载实际功能。它们携带的事实仍然有用:token 用量应当保留以供计费,错误的步骤编号也不应悄然消失。简化的方式是将这些事实折叠进消费方本已必须理解的邻近事件,而非减少记录的信息量。

View File

@@ -2,5 +2,5 @@
# 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
2026-06-20-prune-dead-seam-methods.md: 70596d908bb1d7559e876d93bce0e25874ce1ff0
2026-06-20-prune-dead-seam-methods.zh.md: 1fbc7dbf2f7abb5b17b1df10ef06e490b11335a4
2026-06-20-prune-dead-seam-methods.md: 91a18b4c327d2f3cb636d6d9e0c26e4246a6ffd2
2026-06-20-prune-dead-seam-methods.zh.md: b962bc45052b723eefa533b98fe85f4508d6b6a7

View File

@@ -12,7 +12,7 @@ A capability seam ([interface / implementation / consumer](../architecture/2026-
### `SessionPersistence.has()` and `.delete()`
The abstract service declared its operations beyond create/append: `load`, `list`, `has`, `delete`. Production consumers of `ctx.sessionPersistence` use only two: the agent-loop resume path calls `load()` ([packages/core/agent-loop/src/index.ts:176](../../../../packages/core/agent-loop/src/index.ts)), and the ACP bridge calls `list()` for `session/list` ([packages/ui/acp/src/index.ts:494](../../../../packages/ui/acp/src/index.ts)). Grepping every `sessionPersistence.*` / `persistence.*` use across `packages/*/src` and `examples/` finds no `has(` and no `delete(` on the service. The `.has(`/`.delete(` calls in `packages/ui/acp/src/index.ts` are on the in-memory `SessionStore` and a local `Set` of loading ids, not persistence. The only callers of `has`/`delete` were the contract suites and per-backend specs.
The abstract service declared its operations beyond create/append: `load`, `list`, `has`, `delete`. Production consumers use `load()` and `list()` for resume and session discovery, while no production caller uses persistence `has()` or `delete()`. The similarly named in-memory collection calls in protocol and UI code are unrelated. The only callers of persistence `has`/`delete` were the contract suites and per-backend specs.
`has()` was not just unused: it added a tracked-vs-untracked coordinator probe and a contract branch even though `loadStored(id)` already owns durable existence checks. `delete()` dragged the `deleteStored` backend hook that every backend had to implement. This is the [drop-mutable-session-summary](2026-06-19-drop-mutable-session-summary.md) pattern: a contract test exercised both, but no shipping code asks "is this session persisted?" or removes one.
@@ -33,7 +33,7 @@ Re-adding a seam method with a live consumer is cheap and better-designed than t
## Verification
`has`/`delete`/`deleteStored` are gone from the persistence seam, impl, and contract suites with no new dead exports; the remaining operations (`create`/`append`/`load`/`list`) are untouched, with ACP `session/list` and crash-recovery behaving identically; and the seam README and `docs/architecture.md` list only the surviving methods.
`has`/`delete`/`deleteStored` are gone from the persistence seam, impl, and contract suites with no new dead exports; the remaining operations (`create`/`append`/`load`/`list`) are untouched, with persistence-backed session queries and crash recovery behaving identically; and the seam README and `docs/architecture.md` list only the surviving methods.
## Consequences

View File

@@ -12,7 +12,7 @@ Status: implemented
### `SessionPersistence.has()` 与 `.delete()`
该抽象服务在 create/append 之外声明了更多操作:`load`、`list`、`has`、`delete`。`ctx.sessionPersistence` 的生产消费方只用了两个:agent loop(智能体循环)的恢复路径调用 `load()`([packages/core/agent-loop/src/index.ts:176](../../../../packages/core/agent-loop/src/index.ts)),ACP(Agent Client Protocol)桥接层为 `session/list` 调用 `list()`([packages/ui/acp/src/index.ts:494](../../../../packages/ui/acp/src/index.ts))。在 `packages/*/src` 和 `examples/` 中 grep 所有 `sessionPersistence.*` / `persistence.*` 的使用,找不到对该服务的 `has(` 或 `delete(` 调用。`packages/ui/acp/src/index.ts` 中的 `.has(`/`.delete(` 调用作用于内存中的 `SessionStore` 和一个本地的 loading id `Set`,而非持久化。`has`/`delete` 的唯一调用者是契约测试套件和各后端的 spec。
该抽象服务在 create/append 之外声明了更多操作:`load`、`list`、`has`、`delete`。生产消费方用 `load()` 和 `list()` 完成恢复与会话发现,而没有任何生产调用方使用持久化的 `has()` 或 `delete()`。协议和 UI 代码中名称相似的内存集合调用与此无关。持久化 `has`/`delete` 的唯一调用者是契约测试套件和各后端的 spec。
`has()` 不仅未被使用:在 `loadStored(id)` 已负责持久化存在性检查的情况下,它仍增加了协调器的已跟踪/未跟踪探测和一个契约分支。`delete()` 则拖入每个后端都必须实现的 `deleteStored` 后端钩子。这属于[删除可变会话 summary](2026-06-19-drop-mutable-session-summary.md) 的同类模式:契约测试覆盖了两者,但已发布代码从不会询问“这个会话是否已持久化?”或删除某个会话。
@@ -33,7 +33,7 @@ Status: implemented
## 验证
`has`/`delete`/`deleteStored` 已从持久化 seam、实现和契约测试套件中移除,没有新增无用导出;剩余操作(`create`/`append`/`load`/`list`)未受影响,ACP `session/list` 和崩溃恢复行为完全一致;seam README 和 `docs/architecture.md` 仅列出存留的方法。
`has`/`delete`/`deleteStored` 已从持久化 seam、实现和契约测试套件中移除,没有新增无用导出;剩余操作(`create`/`append`/`load`/`list`)未受影响,基于持久化的会话查询和崩溃恢复行为完全一致;seam README 和 `docs/architecture.md` 仅列出存留的方法。
## 后果

View File

@@ -2,5 +2,5 @@
# 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
2026-06-20-public-agent-stop-surface.md: 81a21de30bfbc25688069efbffb21647889b1bdb
2026-06-20-public-agent-stop-surface.zh.md: 1f5a9ea3c1c3dd6f598c258574a81e52e291f3b7
2026-06-20-public-agent-stop-surface.md: e22c4389df18f3c9ca96763fc097eabefcc5b761
2026-06-20-public-agent-stop-surface.zh.md: e2647b498a8c906579b4fd2b50f94d1c326fe784

View File

@@ -18,7 +18,7 @@ The extra surface area made the loop carry a public verb that is mostly a teardo
`cancel()` is the only public *stop* primitive on `Agent`. Lifecycle owners use `AgentHandle.dispose()` to stop and unregister an agent; non-owners use `cancel()` to abandon current and queued work. The implementation keeps a private turn cancellation holder, but it is not part of the plugin-facing `Agent` contract.
`whenIdle()` is **retained** as the public quiescence-observation primitive (resolve once the agent settles out of `running`, resolve immediately when already idle, await the loop exit when disposed). It is not a stop verb; it is how a non-owner observes the stop *completing* without disposing the agent. Its live consumers are ACP and agent tests that await settlement through this public seam (`packages/ui/acp/tests`, `packages/core/agent-loop/tests`); the production ACP bridge owns its agents and tears them down through `AgentHandle.dispose()`, so `packages/ui/acp/src` itself has no `whenIdle()` call.
`whenIdle()` is **retained** as the public quiescence-observation primitive (resolve once the agent settles out of `running`, resolve immediately when already idle, await the loop exit when disposed). It is not a stop verb; it is how a non-owner observes the stop *completing* without disposing the agent. Its live consumers are ACP and agent tests that await settlement through this public seam (`packages/acp/acp/tests`, `packages/core/agent-loop/tests`); the production ACP bridge owns its agents and tears them down through `AgentHandle.dispose()`, so `packages/acp/acp/src` itself has no `whenIdle()` call.
Public `abort()` is absent, and the disposer remains async and waits for the loop to stop. Tests exercise cancellation through the public typed cause and explicit signal seams rather than reaching into the holder.

View File

@@ -18,7 +18,7 @@ Status: implemented
`cancel()` 是 `Agent` 上唯一的公共*停止*原语。生命周期拥有者使用 `AgentHandle.dispose()` 停止并注销 agent;非拥有者使用 `cancel()` 放弃当前和已排队工作。实现保留一个私有轮次取消 holder,但它不属于面向插件的 `Agent` 契约。
`whenIdle()` **保留**为公开的完全停稳观测原语(agent 从 `running` 状态稳定后 resolve,已处于 idle 时立即 resolve,dispose 后等待循环退出)。它不是停止动词;它是非所有者在不 dispose agent 的前提下观测停止*完成*的方式。它的活跃消费方是 ACP 和通过此公开 seam 等待结算的 agent 测试(`packages/ui/acp/tests`、`packages/core/agent-loop/tests`);生产环境的 ACP 桥接层拥有其 agent 并通过 `AgentHandle.dispose()` 销毁它们,因此 `packages/ui/acp/src` 本身没有 `whenIdle()` 调用。
`whenIdle()` **保留**为公开的完全停稳观测原语(agent 从 `running` 状态稳定后 resolve,已处于 idle 时立即 resolve,dispose 后等待循环退出)。它不是停止动词;它是非所有者在不 dispose agent 的前提下观测停止*完成*的方式。它的活跃消费方是 ACP 和通过此公开 seam 等待结算的 agent 测试(`packages/acp/acp/tests`、`packages/core/agent-loop/tests`);生产环境的 ACP 桥接层拥有其 agent 并通过 `AgentHandle.dispose()` 销毁它们,因此 `packages/acp/acp/src` 本身没有 `whenIdle()` 调用。
公共 `abort()` 已不存在,disposer 仍为异步并等待循环停止。测试通过公共类型化原因和显式 signal seam 验证取消,而不会伸入 holder 内部。

View File

@@ -2,5 +2,5 @@
# 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
2026-06-20-remove-agent-boundary-mirror-events.md: 8c5bb74f2347fe0269cbb9c6504137de761ab919
2026-06-20-remove-agent-boundary-mirror-events.zh.md: 1bb166b7f4453b4f0605e2b1b9120bc5a05266b0
2026-06-20-remove-agent-boundary-mirror-events.md: 31acbb122cf56adfdcfcbce602d09a35f7f13e17
2026-06-20-remove-agent-boundary-mirror-events.zh.md: f386e8d1ccfc2df094645a3cf9ae9524091d2a4e

View File

@@ -15,7 +15,7 @@ English | [中文](2026-06-20-remove-agent-boundary-mirror-events.zh.md)
## Problem
The loop records the canonical transcript in `SessionEvent` and also emitted a parallel set of live `agent/*` boundary mirror events: `agent/turn-start`, `agent/turn-end`, `agent/step-start`, and `agent/step-end`. The mirrors made consumers choose between two sources of truth for the SAME durable fact. ACP already chose the session log for the editor-facing transcript because it is the one durable, replayable record; consuming a live mirror would require reconciling its timing with the boundary already stored in that log. The stdio UI was the only production consumer that still rendered turn boundaries from the mirror events; it already rendered tool calls and results from `session/event`.
The loop records the canonical transcript in `SessionEvent` and also emitted a parallel set of live `agent/*` boundary mirror events: `agent/turn-start`, `agent/turn-end`, `agent/step-start`, and `agent/step-end`. The mirrors made consumers choose between two sources of truth for the SAME durable fact. ACP already chose the session log for prompt settlement and committed output because it is the one durable, replayable record; consuming a live mirror would require reconciling its timing with the boundary already stored in that log. The stdio UI was the only production consumer that still rendered turn boundaries from the mirror events; it already rendered tool calls and results from `session/event`.
This duplication is not free. Every lifecycle change had to update the session event, the mirror event, docs, invariants, tests, and snapshot expectations. The duplicate boundary events also made failure ordering subtle: a turn can be durably closed before a live `agent/turn-end` listener runs, so a post-boundary listener failure has no valid in-log position left and must be reported out of band.

View File

@@ -15,7 +15,7 @@ Status: implemented
## 问题
循环在 `SessionEvent` 中记录规范 transcript(文本记录),同时还发出一组并行的实时 `agent/*` 边界镜像事件:`agent/turn-start`、`agent/turn-end`、`agent/step-start` 和 `agent/step-end`。这些镜像迫使消费方在同一持久事实的两个真源之间做选择。ACP(Agent Client Protocol)已经为面向编辑器的 transcript 选择会话日志,因为它是唯一持久、可重放的记录;消费实时镜像需要把它的时序与日志中已经存储的边界进行调和。stdio UI 是唯一仍从镜像事件渲染轮次边界的生产消费方;它已经从 `session/event` 渲染工具调用和结果。
循环在 `SessionEvent` 中记录规范 transcript(文本记录),同时还发出一组并行的实时 `agent/*` 边界镜像事件:`agent/turn-start`、`agent/turn-end`、`agent/step-start` 和 `agent/step-end`。这些镜像迫使消费方在同一持久事实的两个真源之间做选择。ACP(Agent Client Protocol)已经为提示词结算和已提交输出选择会话日志,因为它是唯一持久、可重放的记录;消费实时镜像需要把它的时序与日志中已经存储的边界进行调和。stdio UI 是唯一仍从镜像事件渲染轮次边界的生产消费方;它已经从 `session/event` 渲染工具调用和结果。
这种重复并非零成本。每次生命周期变更都需要同时更新会话事件、镜像事件、文档、不变式、测试和快照预期。重复的边界事件还使失败排序变得微妙:一个轮次可能在实时 `agent/turn-end` 监听器运行之前就已被持久化关闭,因此边界之后的监听器失败在日志中已没有合法位置可以插入,只能带外上报。

View File

@@ -2,5 +2,5 @@
# 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
2026-07-02-remove-stream-chunk-mirror.md: 62421bfd6032d3be6e942c00abefe17ec947c986
2026-07-02-remove-stream-chunk-mirror.zh.md: 6e006a2c1930e47a651813252d3fd2c4471e6e9e
2026-07-02-remove-stream-chunk-mirror.md: 8b26589a9e89f83d631fa98e801a8d3e08e105d0
2026-07-02-remove-stream-chunk-mirror.zh.md: fcd8c53b8b1b2a5b81f9f929ffd9e06ff6128e45

View File

@@ -27,7 +27,7 @@ The premise the deferral hinged on is settled: chunk persistence is authoritativ
Remove `agent/stream-chunk` from the agent event taxonomy. The token stream is read off `session/event` as `assistant/chunk`, the same feed persistence and replay already use — `session/event` is the single live transcript stream (assistant chunks, turn/step boundaries, tool activity, todos).
**Consumers.** The only production consumer that mattered — the ACP bridge (`dsh-acp`), the real editor-facing streaming surface — already renders `assistant/chunk` off `session/event`, never `agent/stream-chunk`, so it is unaffected. The stdio UI (`dsh-ui-stdio`, a disposable test REPL) was the sole live consumer; it already had a `session/event` listener (from the boundary migration), so its chunk rendering folded into that listener as an `assistant/chunk` case. Consolidating to one listener also removed a latent hazard: the `inReasoning` dim-SGR flag was previously shared across two separate listeners (`agent/stream-chunk` and `session/event`), so a chunk and a boundary racing on it had no defined order; a single listener over the append order makes the interleaving deterministic.
**Consumers.** Persistence, replay, and interactive renderers consume the authoritative session stream directly. The [automation-only ACP bridge](2026-07-23-acp-automation-only-protocol.md) emits committed `assistant/message` text rather than raw chunks, so it needs neither event. No production consumer requires an `Agent`-first token mirror.
## Scope

View File

@@ -27,7 +27,7 @@ ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror
从 agent 事件分类体系中移除 `agent/stream-chunk`。token 流通过 `session/event` 以 `assistant/chunk` 的形式读取——持久化与回放已经使用的正是同一个序列。`session/event` 是唯一的实时 transcript(文本记录)流(assistant 分片、轮次/步骤边界、工具活动、todo)。
**消费方。** 唯一重要的生产消费方——ACP(Agent Client Protocol)桥接(`dsh-acp`,面向编辑器的真实流式输出接口)——已经从 `session/event` 渲染 `assistant/chunk`,从未使用 `agent/stream-chunk`,因此不受影响。stdio UI(`dsh-ui-stdio`,一个一次性的测试 REPL)是唯一的实时消费方;它在边界迁移时已经有了 `session/event` 监听器,因此其分片渲染被折叠进该监听器作为 `assistant/chunk` 分支。合并为一个监听器还消除了一个潜在隐患:`inReasoning` dim-SGR 标志此前在两个独立监听器(`agent/stream-chunk` 和 `session/event`)之间共享,分片与边界在该标志上竞争时没有确定的顺序;单一监听器按追加顺序处理,使交错变为确定性的。
**消费方。** 持久化、回放和交互式渲染器直接消费权威的会话流。[仅面向自动化的 ACP(Agent Client Protocol)桥接层](2026-07-23-acp-automation-only-protocol.md)发出已提交的 `assistant/message` 文本而非原始分片,因此两种事件它都不需要。没有生产消费方需要一个 `Agent` 优先的 token 镜像。
## 范围

View File

@@ -2,5 +2,5 @@
# 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
2026-07-04-drop-image-content-block.md: 566803ab5b9f213b7dc87fcf779ed7a963d988fd
2026-07-04-drop-image-content-block.zh.md: 667ddbc1b7bce2a276fe7528f46d3e43916bad83
2026-07-04-drop-image-content-block.md: cdedf4bd5dfe60c72cea185d88b83d3b93928ff0
2026-07-04-drop-image-content-block.zh.md: 683fd1cdb47e3fcd68ff601c0b0ce4b46f8b06d2

View File

@@ -6,17 +6,17 @@ English | [中文](2026-07-04-drop-image-content-block.zh.md)
## Problem
`ImageBlock` (`packages/llm/llm/src/types.ts`) had no production producer, and every consumer on every path DROPPED it: the deepseek adapter's serializer skipped image blocks (a documented MVP limitation), the pi-ai converter skipped them as unrepresentable, the ACP codec neither advertises image prompt capability nor forwarded image blocks outbound and REJECTS image prompt content inbound, and the compaction estimator charged a flat token constant and rendered `[image]`. An `ImageBlock` constructed then would silently vanish from the wire — the vocabulary advertised a capability no path honored, which is the silent-data-loss shape AGENTS.md's defensive patterns warn against. The only constructors anywhere were tests pinning the skip/drop/estimate branches.
`ImageBlock` (`packages/llm/llm/src/types.ts`) had no production producer, and every consumer on every path DROPPED it: the DeepSeek adapter's serializer skipped image blocks (a documented MVP limitation), the pi-ai converter skipped them as unrepresentable, and the compaction estimator charged a flat token constant and rendered `[image]`. ACP independently rejected image prompt content. An `ImageBlock` constructed then would silently vanish from the provider wire — the vocabulary advertised a capability no path honored, which is the silent-data-loss shape AGENTS.md's defensive patterns warn against. The only constructors anywhere were tests pinning the skip/drop/estimate branches.
## Decision
Remove `ImageBlock`, its map entry, and image-specific branches from adapters, ACP rendering, and compaction. Update the owning vocabulary docs and generated references in the same change. Unknown extension blocks still exercise default branches, and ACP continues to reject inbound image prompt content independently of the harness vocabulary.
Remove `ImageBlock`, its map entry, and image-specific branches from adapters and compaction. Update the owning vocabulary docs and generated references in the same change. Unknown extension blocks still exercise default branches, and ACP continues to reject inbound image prompt content independently of the harness vocabulary.
## Alternatives considered
### Why not keep it?
`ContentBlockMap` can reintroduce images when adapters, ACP, and compaction all support them. Keeping a core type whose only implementation is rejection would advertise an unusable surface; absence gives producers an immediate compile-time failure instead.
`ContentBlockMap` can reintroduce images when adapters and compaction support them. ACP may remain a text-only automation protocol. Keeping a core type whose only implementation is rejection would advertise an unusable surface; absence gives producers an immediate compile-time failure instead.
The recorded fallback, had review landed on keeping the slot: keep `ImageBlock` but replace every silent skip with a loud rejection, and document that policy in the vocabulary — the silent drop was the one state with no defender. Review landed on removal; the fallback stands as the documented alternative should the slot ever return ahead of a full feature.
@@ -26,4 +26,4 @@ No harness `ImageBlock` is constructed outside Agent Note records. ACP's indepen
## Consequences
Re-adding a core vocabulary type later touches several packages at once — but that coordinated change is the shape a real multimodal feature needs anyway (adapter mapping, ACP advertisement, compaction pricing), and none of it existed to preserve.
Re-adding a core vocabulary type later touches several packages at once — but that coordinated change is the shape a real multimodal feature needs anyway (adapter mapping and compaction pricing), and none of it existed to preserve.

View File

@@ -6,17 +6,17 @@ Status: implemented
## 问题
`ImageBlock`(`packages/llm/llm/src/types.ts`)没有任何生产环境的生产者,而每条路径上的每个消费方都将其丢弃:deepseek 适配器的序列化器跳过 image 块(这是文档中注明的 MVP 限制);pi-ai 转换器因无法表示而跳过;ACP(Agent Client Protocol)编解码器既不宣告图像提示词能力、也不向外转发 image 块,并且会拒绝入站的图像提示词内容;压缩(compaction)估算器对其收取一个固定 token 常量并渲染为 `[image]`。此时构造的 `ImageBlock` 会在协议格式(wire format)上静默消失——词汇宣告了一种没有任何路径兑现的能力,这正是 AGENTS.md 防御性模式所警告的静默数据丢失形态。唯一的构造调用出现在测试中,用于覆盖 skip/drop/estimate 分支。
`ImageBlock`(`packages/llm/llm/src/types.ts`)没有任何生产环境的生产者,而每条路径上的每个消费方都将其丢弃:DeepSeek 适配器的序列化器跳过 image 块(这是文档中注明的 MVP 限制);pi-ai 转换器因无法表示而跳过;压缩(compaction)估算器对其收取一个固定 token 常量并渲染为 `[image]`。ACP(Agent Client Protocol)独立地拒绝图像提示词内容。此时构造的 `ImageBlock` 会在提供方协议格式(wire format)上静默消失——词汇宣告了一种没有任何路径兑现的能力,这正是 AGENTS.md 防御性模式所警告的静默数据丢失形态。唯一的构造调用出现在测试中,用于覆盖 skip/drop/estimate 分支。
## 决策
移除 `ImageBlock`、其 map 条目,以及适配器、ACP 渲染和压缩中的 image 专用分支。在同一个变更中更新所属的词汇文档与生成的引用。未知扩展块仍然覆盖默认分支,ACP 继续独立于 harness 词汇拒绝入站的图像提示词内容。
移除 `ImageBlock`、其 map 条目,以及适配器和压缩中的 image 专用分支。在同一个变更中更新所属的词汇文档与生成的引用。未知扩展块仍然覆盖默认分支,ACP 继续独立于 harness 词汇拒绝入站的图像提示词内容。
## 曾考虑的替代方案
### 为什么不保留?
当适配器、ACP 和压缩全部支持 image 时,`ContentBlockMap` 可以重新引入。保留一个唯一实现就是拒绝的核心类型,等于宣告一个不可用的对外服务接口;移除后,生产者会立即得到编译期错误。
当适配器和压缩支持 image 时,`ContentBlockMap` 可以重新引入。ACP 可以继续作为纯文本的自动化协议。保留一个唯一实现就是拒绝的核心类型,等于宣告一个不可用的对外服务接口;移除后,生产者会立即得到编译期错误。
评审中记录的回退方案(假如评审决定保留该槽位):保留 `ImageBlock`,但将所有静默跳过替换为显式拒绝,并在词汇文档中记录该策略——静默丢弃是唯一没有辩护者的状态。评审最终决定移除;此回退方案作为文档化的替代方案保留,以备该槽位在完整功能就绪之前回归。
@@ -26,4 +26,4 @@ Status: implemented
## 后果
日后重新添加核心词汇类型需要同时改动多个包(package)——但这种协调变更本就是真正的多模态功能所需的形态(适配器映射、ACP 能力宣告、压缩定价),而当前并不存在需要保留的实现。
日后重新添加核心词汇类型需要同时改动多个包(package)——但这种协调变更本就是真正的多模态功能所需的形态(适配器映射与压缩定价),而当前并不存在需要保留的实现。

View File

@@ -1,30 +0,0 @@
# Agent Note: Fold the stdio UI helper into the stdio app
Status: implemented
English | [中文](2026-07-04-fold-stdio-ui-helper.zh.md)
The later [redundant-agent removal](2026-07-20-remove-stdio-and-echo-agents.md) supersedes this package-placement decision and removes the folded package, app, and line-oriented surface entirely.
## Problem
The readline UI was a whole package (`@deepseek-ai/dsh-ui-stdio` under `packages/support/`) whose only runtime importer was the app package `@deepseek-ai/dsh-stdio-demo`. The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference was mechanical or descriptive surface that existed BECAUSE the package boundary existed — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. The ui group README recorded the support placement rationale ("exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product"), which left a standing tension: a shipped product app depending on a support package documented as NOT product surface.
The boundary bought package metadata, workspace and tsconfig references, module-graph rows, README entries, and publint surface for a helper that is not independently swappable: the stdio app's front-door cluster always includes the readline UI, and nothing else can meaningfully consume it.
## Decision
At the time, the helper moved into `@deepseek-ai/dsh-stdio` as the terminal-channel plugin. `createStdioChat`, its `StdioRuntime` test seam, and its unit tests moved with it, keeping EOF handling, rendering, disposal, and piped-vs-TTY behavior under the per-file coverage gate without hijacking process globals. The module kept the named `name`/`inject`/`Config`/`apply` export shape consumed by the app mount, while the then-current Echo and REPL Loader smokes proved the composed tree and the plugin-shape suite pinned explicit `unwrapExports` behavior. The superseding removal note above owns the current package and example state.
The earlier support helper package was removed: its manifest, tsconfig references, module-graph rows, and README rows disappeared, while the remaining documentation described the in-package module.
## Alternatives considered
### Why not promote it to `ui/` instead?
Promotion would have resolved the support-vs-product mismatch while keeping the boundary — the right call only if the readline UI were an independently swappable integration or had a second composer, and the consumer census said neither. The structured ACP bridge stays its own package because it is the product protocol surface with its own contract and snapshot tiers; the readline helper is scaffolding for one app's front door. Re-extraction stays cheap pre-release: if a second product app wants the readline UI, split it back out then, with that consumer shaping the package contract.
## Consequences
- The stdio app owns its whole front door; a leaf `cordis.yml` still loads one app package and nothing changed shape for the demos.
- A future standalone terminal UI that wants the helper as a package reintroduces it with that second consumer, rather than the repo keeping a boundary for hypothetical reuse.

View File

@@ -1,30 +0,0 @@
# Agent Note: 将 stdio UI 辅助模块折入 stdio 应用
Status: implemented
[English](2026-07-04-fold-stdio-ui-helper.md) | 中文
后来的[冗余 agent(智能体)移除](2026-07-20-remove-stdio-and-echo-agents.md)取代了这项包放置决策,并完整移除合并后的包、应用和面向行的表面。
## 问题
readline UI 曾是一个完整的包(`packages/support/` 下的 `@deepseek-ai/dsh-ui-stdio`),其唯一的运行时导入方是应用包 `@deepseek-ai/dsh-stdio-demo`。示例通过加载应用来使用 readline UI,从不自行组合该辅助模块;仓库中所有其他引用都是因为包边界存在而存在的机械性或描述性表面:manifest(元数据清单)与 tsconfig 条目、生成的 module-graph 行、依赖图与 README 行,以及命名该包的文档注释。ui 组 README 记录了 support 放置的理由("主要为示例和覆盖率门禁而存在,`ui/` 保留给作为产品交付的界面"),这留下了一个持续的张力:一个已交付的产品应用依赖一个被明确标注为非产品表面的 support 包。
这条边界换来的是:包元数据、workspace 与 tsconfig 引用、module-graph 行、README 条目,以及 publint 表面——服务于一个并不可独立替换的辅助模块:stdio 应用的前门集群始终包含 readline UI,且没有其他消费方能有意义地使用它。
## 决策
当时,该辅助函数移入 `@deepseek-ai/dsh-stdio`,成为终端通道插件。`createStdioChat`、其 `StdioRuntime` 测试 seam 和单元测试随之一同迁移,使 EOF 处理、渲染、释放以及管道/TTY 行为继续受逐文件覆盖率门禁约束,而不会劫持进程全局量。该模块保留应用挂载所消费的具名 `name`/`inject`/`Config`/`apply` 导出形状;当时的 Echo 和 REPL Loader 冒烟证明组合树,插件形状套件则固定显式 `unwrapExports` 行为。上方取代本文的移除记录负责当前包和示例状态。
早期的支持辅助包已移除:其清单、tsconfig 引用、模块图行和 README 行均已消失,其余文档改为描述包内模块。
## 曾考虑的替代方案
### 为什么不将其提升到 `ui/` 而是折入?
提升可以解决 support 与 product 之间的错位,同时保留边界——只有在 readline UI 是一个可独立替换的集成或有第二个组合方时才是正确选择,而消费方普查表明两者皆非。结构化的 ACP(Agent Client Protocol)桥接保留为独立包,因为它是具有自身契约和快照层级的产品协议表面;readline 辅助模块只是一个应用前门的脚手架。在发布前重新拆分成本很低:如果将来有第二个产品应用需要 readline UI,届时再拆出来,由那个消费方来塑造包契约。
## 后果
- stdio 应用完整拥有自己的前门;叶子 `cordis.yml` 仍然只加载一个应用包,演示的形态没有变化。
- 未来如果有独立的终端 UI 需要将该辅助模块作为包使用,届时由那个第二消费方驱动重新引入,而非仓库为假设性的复用保留一条边界。

View File

@@ -2,5 +2,5 @@
# 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
2026-07-04-trim-acp-bridge-unreachable-surface.md: ce0c623930192d02c8347c3956c497ee13048904
2026-07-04-trim-acp-bridge-unreachable-surface.zh.md: e89b0e7c9c7c8438fb71adacf1b525a0b94bf04b
2026-07-04-trim-acp-bridge-unreachable-surface.md: 959cad37f279888fda79b1632c3553ea122803c5
2026-07-04-trim-acp-bridge-unreachable-surface.zh.md: 9127b7f167c3e5c74cdb99266828c20e79f3cb90

View File

@@ -4,16 +4,18 @@ Status: implemented
English | [中文](2026-07-04-trim-acp-bridge-unreachable-surface.zh.md)
> The handshake-identity simplification remains current. The generic-card fallback was removed when [ACP became automation-only](2026-07-23-acp-automation-only-protocol.md); UI transports retain the provider-neutral presentation contract.
## Problem
Two pieces of `dsh-acp` surface were unreachable from any shipped configuration:
1. **`AcpConfig.agentName` / `agentVersion`** (`packages/ui/acp/src/index.ts`). The shipped app package hands the bridge only `{ model }` (`packages/examples/acp-demo/src/index.ts`), so no leaf `cordis.yml` — the only production config surface — could set the knobs at all; they were settable solely by direct-mounting the bridge, which only a unit test did. Every snapshot expected output — the hook-matrix scenarios included — pins the schema defaults (`deepseek-harness-acp` / `0.0.1`). The pair also carried a live `TODO(double-default)`: the literals existed twice (schema `.default(...)` plus `??` fallbacks), with the TODO asking to pick one home.
1. **`AcpConfig.agentName` / `agentVersion`** (`packages/acp/acp/src/index.ts`). The shipped app package hands the bridge only its agent target (`packages/examples/acp-demo/src/index.ts`), so no leaf `cordis.yml` — the only production config surface — could set the knobs at all; they were settable solely by direct-mounting the bridge, which only a unit test did. Every snapshot expected output — the hook-matrix scenarios included — pins the schema defaults (`deepseek-harness-acp` / `0.0.1`). The pair also carried a live `TODO(double-default)`: the literals existed twice (schema `.default(...)` plus `??` fallbacks), with the TODO asking to pick one home.
2. **The `toolKindFor` name heuristic** (same file) special-cased `bash*`/`read*`/`write`/`edit*` tool names in the generic-fallback path. Since the [render-intent union](../architecture/2026-07-02-tool-render-intent-union.md), every first-party tool those arms matched ships its own `presentCall` carrying its kind, and the presenter-less production tools (`subagent`, `subagent_fork`) fell through to `other` anyway. The arms were production-reachable only when a tool declined to present its own call — a `presentCall` that THROWS (the containment fallback), or model arguments that fail the tool's schema so `defineTool`'s `presentCall` wrapper returns `undefined` (e.g. a `bash` call missing the required `description`) — and the bridge's own module doc states the design rule the heuristic violated: "the bridge never special-cases tool names".
## Decision
Hardcode the existing handshake identity `{ name: 'deepseek-harness-acp', version: '0.0.1' }` at initialization and remove the unreachable config fields and duplicate defaults. Replace `toolKindFor` with neutral `'other'` at both presenter fallbacks. Normal first-party presentations are unchanged; malformed or failed presentations now render an honest generic card instead of inferring a kind from the tool name. Initialize tests and snapshots pin the handshake; only the malformed calls in `hook-codex-posttool-block` change fallback card kind.
Hardcode the existing handshake identity `{ name: 'deepseek-harness-acp', version: '0.0.1' }` at initialization and remove the unreachable config fields and duplicate defaults. The original implementation also replaced `toolKindFor` with neutral `'other'` at both presenter fallbacks; ACP no longer projects tool cards, so that fallback has left the transport entirely. Initialize tests and snapshots pin the handshake.
## Alternatives considered
@@ -23,4 +25,4 @@ Branding can return when the app package exposes it to deployments. Inferring pr
## Consequences
Nothing beyond the fallback rendering trade described above — degenerate paths whose neutral card is more diagnosable than an inferred first-party one.
The bridge exposes no branding knobs. UI transports own generic presentation fallback without tool-name inference, while ACP carries no tool-card surface.

View File

@@ -4,16 +4,18 @@ Status: implemented
[English](2026-07-04-trim-acp-bridge-unreachable-surface.md) | 中文
> 握手标识简化仍然有效。通用卡片回退已随 [ACP 转为仅面向自动化](2026-07-23-acp-automation-only-protocol.md)一并移除;UI 传输层保留提供方无关的展示契约。
## 问题
`dsh-acp` 有两处对外表面在任何已交付的配置中都不可达:
1. **`AcpConfig.agentName` / `agentVersion`**(`packages/ui/acp/src/index.ts`)。已发布应用包只向 bridge 传递 `{ model }`(`packages/examples/acp-demo/src/index.ts`),因此没有任何叶子 `cordis.yml`——唯一的生产配置表面——能够设置这些配置项;只有直接挂载 bridge 才能设置它们,而这种做法只存在于一个单元测试中。每份快照预期输出——包括钩子矩阵场景——都固定 schema 默认值(`deepseek-harness-acp` / `0.0.1`)。这对配置项还带有一个尚未解决的 `TODO(double-default)`:字面量存在两次(schema `.default(...)` 加 `??` 后备值),TODO 要求为它们选择一个归属。
1. **`AcpConfig.agentName` / `agentVersion`**(`packages/acp/acp/src/index.ts`)。已发布应用包只向 bridge 传递其 agent 的提供方/模型目标(`packages/examples/acp-demo/src/index.ts`),因此没有任何叶子 `cordis.yml`——唯一的生产配置表面——能够设置这些配置项;只有直接挂载 bridge 才能设置它们,而这种做法只存在于一个单元测试中。每份快照预期输出——包括钩子矩阵场景——都固定 schema 默认值(`deepseek-harness-acp` / `0.0.1`)。这对配置项还带有一个尚未解决的 `TODO(double-default)`:字面量存在两次(schema `.default(...)` 加 `??` 后备值),TODO 要求为它们选择一个归属。
2. **`toolKindFor` 名称启发式**(同一文件)在通用回退路径中对 `bash*`/`read*`/`write`/`edit*` 工具名做了特殊处理。自[render-intent 联合类型](../architecture/2026-07-02-tool-render-intent-union.md)以来,这些分支匹配到的每个第一方工具都自带 `presentCall` 并携带其 kind,而没有 presenter 的生产工具(`subagent`、`subagent_fork`)本来就落入 `other`。这些分支只有在工具拒绝自行呈现调用时才在生产中可达:`presentCall` 抛出异常(容错回退),或模型参数未通过工具 schema 导致 `defineTool` 的 `presentCall` 包装层返回 `undefined`(例如 `bash` 调用缺少必需的 `description`)。而桥接层自身的模块文档明确声明了该启发式所违反的设计规则:"桥接层绝不对工具名做特殊处理"。
## 决策
在初始化时硬编码现有的握手标识 `{ name: 'deepseek-harness-acp', version: '0.0.1' }`,移除不可达的配置字段与重复默认值。在两个 presenter 回退处,将 `toolKindFor` 替换为中性的 `'other'`。正常的第一方呈现不受影响;格式错误或失败的呈现现在会渲染一个诚实的通用卡片,而非从工具名推断 kind。初始化测试和快照固定握手标识;只有 `hook-codex-posttool-block` 中格式错误的调用改变了回退卡片的 kind。
在初始化时硬编码现有的握手标识 `{ name: 'deepseek-harness-acp', version: '0.0.1' }`,移除不可达的配置字段与重复默认值。最初的实现还在两个 presenter 回退处将 `toolKindFor` 替换为中性的 `'other'`;ACP 不再投影工具卡片,因此该回退已完全离开传输层。初始化测试和快照固定握手标识。
## 曾考虑的替代方案
@@ -23,4 +25,4 @@ Status: implemented
## 后果
除上述回退渲染的取舍外没有其他影响——退化路径下,中性卡片比推断出的第一方卡片更易于诊断。
桥接层不暴露品牌配置项。UI 传输层拥有不做工具名推断的通用展示回退,而 ACP 不承载任何工具卡片表面。

View File

@@ -2,5 +2,5 @@
# 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
2026-07-20-remove-stdio-and-echo-agents.md: 2aba8193710c96d3726b91062bfa43d039b4cabf
2026-07-20-remove-stdio-and-echo-agents.zh.md: 2c3916683f4743384a2ce4104319da26145837fe
2026-07-20-remove-stdio-and-echo-agents.md: d804f8e3c3de886fc48c87d6378c9f06d0e3c15a
2026-07-20-remove-stdio-and-echo-agents.zh.md: d42e6d527231e1703153c02c66d9286fa1e73c95

View File

@@ -18,8 +18,8 @@ The stdio and Echo agents are removed without compatibility packages, modes, com
The remaining application roles are explicit:
- [`@deepseek-ai/dsh-tui-demo`](../../../../packages/examples/tui-demo/README.md) owns terminal-interactive execution. `examples/tui-agent` owns the complete coding composition, Code Mode overlay, PTY coverage, and terminal snapshots.
- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) owns non-interactive execution. `examples/headless-agent` owns the real-model one-shot composition, replay snapshots, generic real-agent suites, and test-only keyless Loader fixtures.
- [`@deepseek-ai/dsh-tui-demo`](../../../../packages/examples/tui-demo/README.md) owns terminal-interactive execution. It rejects non-TTY streams before Loader boot; `examples/tui-agent` owns the complete coding composition, Code Mode overlay, PTY coverage, and terminal snapshots.
- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) owns non-interactive execution, including pipes. `examples/headless-agent` owns the real-model one-shot composition, replay snapshots, generic real-agent suites, and test-only keyless Loader fixtures.
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) and `@deepseek-ai/dsh-jsonrpc` own their framed protocol integrations.
The SDK project model and create/config workflows replace the `stdio` run-interface option with `tui`; generated TUI projects compose `@deepseek-ai/dsh-tui` and create or resume one exact session. Repository-facing demo documentation requires a DeepSeek API key and leads with the real Headless or TUI agents.
@@ -28,11 +28,14 @@ Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapt
## Verification
TUI and Headless Loader coverage run the real app packages in source and built modes. TUI uses a pseudo-terminal; Headless proves its task/result and tool-call contracts. Generated graphs and repository searches reject stale package, command, leaf, and SDK-interface references.
TUI and Headless Loader coverage run the real app packages in source and built modes. PTY-driven subprocess coverage is reserved for the TUI lifecycle; other entry-point smokes use the one-shot pipe protocol. Headless proves its task/result and tool-call contracts. Generated graphs and repository searches reject stale package, command, leaf, SDK-interface, `createStdioChat`, and `StdioRuntime` references.
The TUI PTY smoke includes the Code Mode overlay composition, while `examples/cordis-agent/tests/keyless-smoke.e2e.ts` provides a minimal PTY boot over the real Cordis-agent Loader tree. The built `dsh` bin rejects a piped TUI launch before Loader boot and points at its one-shot `-p` mode; `apps/cli/tests/built-bin.e2e.ts` pins that path, while `cli-demo`'s built-bin suite runs text, JSON, and structurally parsed `stream-json` output under plain Node, persists fresh sessions, and rejects invalid arguments and missing config without contaminating stdout. Time-context integration uses the real Headless composition for two ordered turns, while its package tests own finer elapsed-time behavior.
## Alternatives considered
- **Keep the line agent only for pipes** — rejected because Headless has a bounded task contract, format-pure stdout, durable completion, and process exit status.
- **Keep, fold, or promote the readline helper as a package** — rejected because it had one app consumer and no independently swappable contract. Folding it into the stdio app removed an unjustified support-package boundary but still retained the redundant product; a future standalone line UI needs a real second consumer before reintroducing that package.
- **Keep Echo as the keyless quick start** — rejected because the first product experience should exercise the real model and supported coding agent, not a scripted adapter with a bespoke tool.
- **Keep Echo only as a CI demo command** — rejected because test-owned Headless fixtures cover the same Loader and built-artifact boundaries without preserving a mock product leaf.
- **Remove every stdio or mock mechanism** — rejected because framed protocols, process I/O, and deterministic test adapters are independent infrastructure, not the removed agents.
@@ -43,3 +46,4 @@ TUI and Headless Loader coverage run the real app packages in source and built m
- The repository has no keyless user-facing agent demo; local agent demos require `DEEPSEEK_API_KEY`.
- CI retains keyless real-entry coverage through test fixtures rather than a product command.
- Existing stdio-agent configurations, Echo commands, and SDK `--interface=stdio` invocations fail instead of being translated.
- Piped multi-turn interaction in one process and the readline provider for non-TTY `ask_user_question` are intentionally gone; resume covers durable multi-turn work, and a non-TTY composition must supply its own interaction provider.

View File

@@ -18,8 +18,8 @@ DeepSeek Harness 在 TUI 和 Headless coding agent 之外,还提供了两个
保留的应用角色均有明确归属:
- [`@deepseek-ai/dsh-tui-demo`](../../../../packages/examples/tui-demo/README.md) 负责终端交互式执行。`examples/tui-agent` 拥有完整 coding 组装、Code Mode 覆盖层、PTY 覆盖和终端快照。
- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) 负责非交互式执行。`examples/headless-agent` 拥有真实模型的单次任务组装、回放快照、通用真实 agent 测试套件,以及仅供测试使用的无密钥 Loader fixture。
- [`@deepseek-ai/dsh-tui-demo`](../../../../packages/examples/tui-demo/README.md) 负责终端交互式执行。它会在 Loader 启动前拒绝非 TTY 流;`examples/tui-agent` 拥有完整 coding 组装、Code Mode 覆盖层、PTY 覆盖和终端快照。
- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) 负责非交互式执行,包括管道方式。`examples/headless-agent` 拥有真实模型的单次任务组装、回放快照、通用真实 agent 测试套件,以及仅供测试使用的无密钥 Loader fixture。
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) 和 `@deepseek-ai/dsh-jsonrpc` 负责各自的分帧协议集成。
SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换为 `tui`;生成的 TUI 工程组合 `@deepseek-ai/dsh-tui`,并创建或恢复一个确切会话。仓库中的演示文档要求 DeepSeek API key,并优先引导到真实的 Headless 或 TUI agent。
@@ -28,11 +28,14 @@ SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换
## 验证
TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。TUI 使用伪终端;Headless 验证任务/结果契约和工具调用契约。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点和 SDK 接口引用。
TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。由 PTY 驱动的子进程覆盖仅用于 TUI 生命周期;其他入口冒烟测试使用单次管道协议。Headless 验证任务/结果契约和工具调用契约。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点、SDK 接口、`createStdioChat` 和 `StdioRuntime` 引用。
TUI PTY 冒烟测试包含 Code Mode 覆盖层组装,而 `examples/cordis-agent/tests/keyless-smoke.e2e.ts` 会基于真实 Cordis-agent Loader 目录树执行最小 PTY 启动。构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向其单次 `-p` 模式;`apps/cli/tests/built-bin.e2e.ts` 固定了该执行路径,而 `cli-demo` 的 built-bin 套件在普通 Node 下运行文本、JSON 和经过结构化解析的 `stream-json` 输出,持久化新建会话,并在不污染 stdout 的情况下拒绝无效参数和缺失配置。时间上下文集成通过真实 Headless 组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。
## 曾考虑的替代方案
- **仅为 pipe 保留面向行 agent**:不予采纳,因为 Headless 已提供有界任务契约、格式纯净的 stdout、持久完成边界和进程退出状态。
- **将 readline helper 作为包保留、折叠或提升**:不予采纳,因为它只有一个 app 消费方,并不存在可独立替换的契约。将它折叠进 stdio app 虽然移除了没有正当理由的支撑包边界,却仍保留了重复产品;将来要重新引入这个包,独立的面向行 UI 必须先有真正的第二个消费方。
- **保留 Echo 作为无密钥快速上手路径**:不予采纳,因为首次产品体验应使用真实模型和受支持的 coding agent,而不是带专用工具的脚本化适配器。
- **只为 CI 演示命令保留 Echo**:不予采纳,因为由测试持有的 Headless fixture 可以覆盖相同的 Loader 和构建产物边界,无需保留 mock 产品叶节点。
- **移除所有 stdio 或 mock 机制**:不予采纳,因为分帧协议、进程 I/O 和确定性测试适配器是独立基础设施,并不是被移除的 agent。
@@ -43,3 +46,4 @@ TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真
- 仓库没有面向用户的无密钥 agent 演示;本地 agent 演示需要 `DEEPSEEK_API_KEY`。
- CI 通过测试 fixture 保留针对真实入口的无密钥覆盖,而不是依赖产品命令。
- 既有 stdio agent 配置、Echo 命令和 SDK `--interface=stdio` 调用会直接失败,不会被转换。
- 有意移除了单进程内基于管道的多轮交互,以及面向非 TTY `ask_user_question` 的 readline 提供方;恢复会话可以满足持久多轮工作,非 TTY 组装则必须自行提供交互提供方。

View File

@@ -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
2026-07-20-retire-readline-front-door.md: 7ebcfdc246bdf6971418609c61acbd4019aa90cb
2026-07-20-retire-readline-front-door.zh.md: cf4d03594ed3a0cf31bed96eb2133bd37959084a

View File

@@ -1,46 +0,0 @@
# Agent Note: Retire the readline front door and the repl-agent example
Status: implemented
English | [中文](2026-07-20-retire-readline-front-door.zh.md)
## Problem
The repo shipped two interactive terminal front doors: the line-oriented readline channel (`@deepseek-ai/dsh-stdio`) and the full-screen [`@deepseek-ai/dsh-tui`](../feature/2026-07-17-dedicated-full-screen-tui-front-door.md). After the TUI landed, readline's interactive role was redundant — `demo:tui` superseded `demo:repl` as the coding-agent experience — while its remaining real role, pipes and automation, was already served better by the one-shot `@deepseek-ai/dsh-cli-demo` app (task in, DSH-native `text`/`json`/`stream-json` out, durable persistence, signal handling).
The duplication was structural, not just cosmetic: `dsh-stdio-demo` carried a `TerminalMode` (`auto`/`readline`/`tui`) selection seam, ~1,000 lines of readline unit tests, a readline transcript grammar (`[tool call] …` lines) that the CI demo smoke and two built-bin e2es grepped, and an inverted example composition where the flagship `tui-agent` leaf was defined as an include-patch over the `repl-agent` leaf it superseded.
## Decision
Delete the readline front door and the repl-agent example; keep exactly three front-door archetypes: **interactive TUI** (TTY-only, fails loud on pipes), **one-shot CLI** (`-p`/positional task, pipes and automation), and **servers** (ACP / JSON-RPC).
- `packages/ui/stdio` and `examples/repl-agent` are gone. `packages/examples/stdio-demo` is renamed `@deepseek-ai/dsh-tui-demo` (`packages/examples/tui-demo`) and always mounts `dsh-tui`; the `TerminalMode`/`resolveTerminalMode`/`ui.mode` seam is deleted. The bin refuses non-TTY streams **before booting the Loader** (a compose-time throw inside a Loader tree is logged per-entry, not rethrown, so a piped launch would otherwise settle into an idle UI-less process instead of exiting nonzero).
- `examples/tui-agent/cordis.yml` now owns the coding composition inline (the include-patch inversion is gone); its Code Mode overlay includes its own base. `examples/cordis-agent` moved to the TUI app.
- `examples/echo-agent` moved to the one-shot `dsh-cli-demo` app; `dsh-cli-demo` gained `-p/--prompt` as the flag form of the single task (mutually exclusive with the positional).
- The UI-independent with-key coding e2es (`full-loop`, `coding-task`, `resume`, `compaction`, `todo-write`, `code-mode` and their shared harness) moved verbatim from `examples/repl-agent/tests/` to `examples/tui-agent/tests/` — they assemble the stack programmatically and never touched a UI.
- The SDK wizard's `stdio` run interface became `tui` (`RunInterface = 'acp' | 'tui' | 'embed'`), contributing a `dsh-tui` entry instead of `dsh-stdio`; the generated `index.ts` guards TTY before `startSDK` for the same pre-boot fail-loud reason as the tui-demo bin.
### Testing policy: PTY only for the TUI
Pipes remain the default test medium. PTY-driven subprocess tests are sanctioned **only** where the subject is the TUI itself: `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` (which gained the Code Mode overlay boot scenario, replacing repl-agent's pipe smoke as the overlay's keyless composition proof) and the minimal PTY boot smoke in `examples/cordis-agent` (whose front door IS the TUI). Everything else moved to pipes over the one-shot bin:
- `examples/echo-agent/tests/echo.e2e.ts` proves the Loader boot + mock-model tool round-trip through `stream-json` records instead of readline transcript lines.
- The CI demo-smoke gate (`scripts/run-gates.ts`, AGENTS.md) runs `demo:echo --output-format stream-json -p "echo ci smoke"` and parses the records structurally.
- `packages/examples/tui-demo/tests/built-bin.e2e.ts` proves the built bin's piped-launch refusal (nonzero exit + pointer at `dsh-cli-demo`); the echo-round-trip-under-plain-Node and missing-config fail-loud proofs live in `cli-demo`'s built-bin suite.
- `packages/context/time-context/tests/time-context.e2e.ts` runs one one-shot turn; multi-turn elapsed rendering stays unit-covered in its spec.
## Accepted losses
- **Piped multi-turn in one process** — the readline channel could script several turns over stdin; the one-shot bin runs one task per process. Multi-turn continuity is covered by `RESUME_SESSION_ID`/resume e2es and the TUI's scripted PTY conversation.
- **Non-TTY `ask_user_question`** — the readline provider was the only non-TTY terminal implementation of `ctx.userInteraction`. A headless run whose model calls `ask_user_question` now fails that tool call (no provider); the ACP bridge remains the non-terminal provider. A future headless deployment that needs it composes its own provider.
## Alternatives considered
- **Keep `dsh-stdio` as a pipe/automation channel without the repl demo** — rejected: its automation role duplicated `dsh-cli-demo` with a weaker contract (unstructured transcript, EOF-exit heuristics vs. one durable turn ending and format-pure output).
- **Rewrite the piped smokes as PTY drivers** — rejected: PTY is the flakier, more complex medium and is reserved for the one surface pipes cannot prove (real TTY takeover/restore).
## Consequences
- One interactive front door (TUI), one automation front door (one-shot CLI), two servers; no mode-selection seam in the terminal app.
- ~1,000 lines of readline unit tests deleted with their behavior; the readline transcript grammar is gone from all gates.
- This supersedes the packaging half of [fold the stdio UI helper](2026-07-04-fold-stdio-ui-helper.md) (the folded package is now deleted) and amends the composition described in [the TUI front-door note](../feature/2026-07-17-dedicated-full-screen-tui-front-door.md) (no `auto` selection; `tui-agent` owns the coding composition).

View File

@@ -1,46 +0,0 @@
# Agent Note: 退役 readline 前端与 repl-agent 示例
Status: implemented
[English](2026-07-20-retire-readline-front-door.md) | 中文
## 问题
仓库同时提供两个交互式终端前端:面向行的 readline 通道(`@deepseek-ai/dsh-stdio`)和全屏的 [`@deepseek-ai/dsh-tui`](../feature/2026-07-17-dedicated-full-screen-tui-front-door.md)。TUI 落地之后,readline 的交互角色已经冗余——`demo:tui` 作为编码 agent 体验取代了 `demo:repl`——而它剩下的真实角色(管道与自动化)已由单次任务的 `@deepseek-ai/dsh-cli-demo` 应用以更好的方式承担(任务输入、DSH 原生 `text`/`json`/`stream-json` 输出、持久化、信号处理)。
这种重复是结构性的,不只是表面问题:`dsh-stdio-demo` 携带一个 `TerminalMode`(`auto`/`readline`/`tui`)选择接缝、约 1,000 行 readline 单元测试、一套被 CI 演示冒烟测试和两个 built-bin e2e 用 grep 匹配的 readline 文本记录语法(`[tool call] …` 行),以及一个倒置的示例组合:旗舰 `tui-agent` 叶节点被定义为对它所取代的 `repl-agent` 叶节点的 include patch。
## 决定
删除 readline 前端和 repl-agent 示例;只保留三类前端原型:**交互式 TUI**(仅 TTY,管道下快速失败)、**单次任务 CLI**(`-p`/位置参数任务,服务管道与自动化)以及**服务器**(ACP / JSON-RPC)。
- `packages/ui/stdio` 与 `examples/repl-agent` 已删除。`packages/examples/stdio-demo` 更名为 `@deepseek-ai/dsh-tui-demo`(`packages/examples/tui-demo`)并始终挂载 `dsh-tui`;`TerminalMode`/`resolveTerminalMode`/`ui.mode` 接缝随之删除。bin 在**启动 loader 之前**就拒绝非 TTY 流(Loader 树内组合期抛出的异常按条目记录日志而不会重新抛出,管道启动否则会沉降为一个空闲的无 UI 进程而不是以非零码退出)。
- `examples/tui-agent/cordis.yml` 现在内联拥有编码组合(include patch 倒置消失);其 Code Mode 覆盖层 include 自己的基础配置。`examples/cordis-agent` 迁移到 TUI 应用。
- `examples/echo-agent` 迁移到单次任务的 `dsh-cli-demo` 应用;`dsh-cli-demo` 新增 `-p/--prompt` 作为单个任务的旗标形式(与位置参数互斥)。
- 与 UI 无关的带密钥编码 e2e(`full-loop`、`coding-task`、`resume`、`compaction`、`todo-write`、`code-mode` 及其共享 harness)原样从 `examples/repl-agent/tests/` 移入 `examples/tui-agent/tests/`——它们以编程方式组装整个栈,从不接触任何 UI。
- SDK 向导的 `stdio` 运行接口改为 `tui`(`RunInterface = 'acp' | 'tui' | 'embed'`),贡献 `dsh-tui` 配置项而不是 `dsh-stdio`;生成的 `index.ts` 在 `startSDK` 之前检查 TTY,理由与 tui-demo bin 的启动前快速失败相同。
### 测试策略:PTY 仅用于 TUI
管道仍是默认测试介质。PTY 驱动的子进程测试**仅**在被测对象就是 TUI 本身时获准使用:`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts`(新增 Code Mode 覆盖层启动场景,取代 repl-agent 的管道冒烟测试成为该覆盖层的无密钥组合证明)和 `examples/cordis-agent` 中最小的 PTY 启动冒烟测试(其前端就是 TUI)。其余全部改为通过单次任务 bin 走管道:
- `examples/echo-agent/tests/echo.e2e.ts` 通过 `stream-json` 记录证明 Loader 启动 + mock 模型的工具往返,而不是匹配 readline 文本记录行。
- CI 演示冒烟门禁(`scripts/run-gates.ts`、AGENTS.md)运行 `demo:echo --output-format stream-json -p "echo ci smoke"` 并结构化解析记录。
- `packages/examples/tui-demo/tests/built-bin.e2e.ts` 证明构建产物 bin 对管道启动的拒绝(非零退出 + 指向 `dsh-cli-demo` 的提示);纯 Node 下的 echo 往返证明与缺失配置的快速失败证明位于 `cli-demo` 的 built-bin 套件。
- `packages/context/time-context/tests/time-context.e2e.ts` 运行一个单次任务轮次;多轮 elapsed 渲染仍由其单元测试覆盖。
## 接受的损失
- **单进程内的管道多轮对话**——readline 通道可以通过 stdin 脚本化多个轮次;单次任务 bin 每个进程只运行一个任务。多轮连续性由 `RESUME_SESSION_ID`/resume e2e 和 TUI 的脚本化 PTY 对话覆盖。
- **非 TTY 的 `ask_user_question`**——readline 提供方是 `ctx.userInteraction` 唯一的非 TTY 终端实现。模型调用 `ask_user_question` 的 headless 运行现在会让该工具调用失败(没有提供方);ACP 桥接仍是非终端提供方。未来需要它的 headless 部署自行组合提供方。
## 曾考虑的替代方案
- **保留 `dsh-stdio` 作为纯管道/自动化通道而只删 repl 演示**——不予采纳:它的自动化角色以更弱的契约重复了 `dsh-cli-demo`(非结构化文本记录、EOF 退出的启发式判断,对比后者的一次持久轮次结束和格式纯净输出)。
- **把管道冒烟测试改写为 PTY 驱动**——不予采纳:PTY 是更易波动、更复杂的介质,仅保留给管道无法证明的那一个表面(真实 TTY 的接管/恢复)。
## 后果
- 一个交互式前端(TUI)、一个自动化前端(单次任务 CLI)、两个服务器;终端应用不再有模式选择接缝。
- 约 1,000 行 readline 单元测试随其行为一起删除;readline 文本记录语法从所有门禁中消失。
- 本决定取代 [fold the stdio UI helper](2026-07-04-fold-stdio-ui-helper.md) 的打包部分(被折叠的包现已删除),并修订 [TUI 前端 Agent Note](../feature/2026-07-17-dedicated-full-screen-tui-front-door.md) 描述的组合(不再有 `auto` 选择;`tui-agent` 拥有编码组合)。

View File

@@ -2,5 +2,5 @@
# 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
2026-07-22-plan-specific-collaboration-state.md: 2fc163213ca0ee1de5633e4d7db14a814b2f7bb2
2026-07-22-plan-specific-collaboration-state.zh.md: 811f657bf31c96dde88e400fc25fe2fe6df1f157
2026-07-22-plan-specific-collaboration-state.md: fb26d15238f0eb1b63fdccc7e48a6c49a44236cf
2026-07-22-plan-specific-collaboration-state.zh.md: 93186eebb263458bc99e7f7562d065fbf9e5d4bf

View File

@@ -1,4 +1,4 @@
# Agent Note: Collapse named session modes into plan mode
# Agent Note: Plan-specific collaboration state
Status: implemented
@@ -8,7 +8,9 @@ English | [中文](2026-07-22-plan-specific-collaboration-state.zh.md)
The first plan-mode implementation introduced a generic named-mode registry even though the product shipped only `plan`. `ModeConfig.modes`, definition-name validation, `ctx.modes.list()`, retired-definition fallback, and a synthetic `review` mode in tests existed only to support hypothetical future collaboration modes. The production-specific behavior—plan guidance, `/plan`, and `exit_plan_mode`—still lived in the same package, so the generic API did not isolate a reusable mechanism from plan policy.
The word “mode” also spans unrelated domains. Sandbox mode is an enforcing policy owned by `ctx.sandboxPolicy` and logged as `sandbox/mode`; plan mode is a collaboration stance that contributes guidance and a reviewed exit. Treating both as instances of one named-mode abstraction would obscure their independent ownership. ACP's protocol happens to expose a generic mode picker, but that is an adapter vocabulary rather than evidence that the harness needs a generic mode domain.
The word “mode” also spans unrelated domains. Sandbox mode is an enforcing policy owned by `ctx.sandboxPolicy` and logged as `sandbox/mode`; plan mode is a collaboration stance that contributes guidance and a reviewed exit. Treating both as instances of one named-mode abstraction would obscure their independent ownership. A transport's generic vocabulary is not evidence that the harness needs a generic mode domain.
Plan mode also needs a durable stance, a reviewable plan artifact, an explicit human boundary, and request reconstruction across resume and fork. Those requirements belong to the plan feature even after the generic registry and interactive ACP projections are removed.
## Decision
@@ -16,10 +18,22 @@ Plan mode owns a plan-specific product package: `@deepseek-ai/dsh-plan-mode` at
Configuration is exactly `{ section: string }`. The package registers the fixed `plan:policy` section, `/plan [message]`, the exact `/plan off` direct-exit form, and `exit_plan_mode` itself. Bare `/plan` selects active; another non-empty argument selects it first and then sends the trimmed text through `agent.steer()`, making the text an ordinary logged user message in the affected step. `/plan off` selects inactive without model input and can cancel an entry that is still pending at the boundary. The exit tool remains registered while plan mode is inactive so the request tool catalog stays stable.
ACP keeps its protocol-level `default` and `plan` ids. The bridge maps those two ids to the boolean service, advertises only that fixed pair, rejects every other id at the adapter boundary, and maps committed `plan/mode` events back to `current_mode_update`. The protocol remains generic without forcing genericity into the product domain.
Human-facing compositions own plan selection and review. This note originally kept ACP's protocol-level `default`/`plan` picker as an adapter over the boolean service; [ACP as an automation-only protocol](2026-07-23-acp-automation-only-protocol.md) supersedes that wire projection, so the ACP composition now mounts neither plan mode nor a mode-selection protocol.
Sandbox mode and approval policy remain separate enforcement axes. Plan mode neither reads nor writes them, and the simplification introduces no shared base type, registry, or preset abstraction across those concepts.
### Boundary and model contract
`plan/mode` is log-only and non-surface, so resume, fork, and compaction recover the state without a live mirror. A spawned agent begins inactive because there is no creation-time plan option. Pending user selections flush before the affected request assembly on prompt submission, ordinary continuation, or a request-recovery retry; a failed durable append leaves the intent pending for a later boundary.
The active state contributes the deployment's section at prompt order 50. Inactive state contributes no section, while `exit_plan_mode` remains registered in both states, so a transition changes the logged request header but not native tool schemas or the Code Mode SDK. A user-driven transition appends one plugin-sourced notice only when the last request header described the opposite state; a pre-first-request or net-zero selection adds none, and an approved tool exit relies on its tool result instead of a second notice.
### Reviewed exit
`exit_plan_mode` requires a calling agent in active plan mode and a non-empty markdown plan beginning with a heading. The user-interaction question carries that exact plan as detail and offers `Approve` or `Keep planning` plus free-text feedback. Only one `Approve` selection with no custom text consents; every other answer stays in plan mode and returns corrective feedback to the model. An approved exit becomes a silent pending selection, leaving plan guidance active for the rest of the current tool batch and removing it before the next request.
The tool renders the submitted plan as a generic card titled by its first heading. An absent or failed user-interaction provider, a failed review, or plugin disposal while review is pending fails closed and leaves manual `/plan off` as the human escape path.
## Deleted surface
- The arbitrary definition map, mode-name regular expression, reserved-name rules, and per-definition command loop.
@@ -31,17 +45,27 @@ Sandbox mode and approval policy remain separate enforcement axes. Plan mode nei
**Keep a private generic registry and expose only plan today.** Rejected because the unused name/config machinery would still be maintained and tested without a second production consumer. A future collaboration state can establish the right shared seam from two concrete cases.
**Fold sandbox mode into the same service.** Rejected because collaboration guidance and execution confinement have different owners, lifecycle semantics, and consumers. Their shared English noun is not a domain relationship.
**Fold sandbox or approval policy into plan state.** Rejected because collaboration guidance, execution confinement, and permission decisions have different owners, lifecycle semantics, and consumers. A mode-owned sandbox cap also makes a user's explicit sandbox selection appear to succeed while silently doing nothing.
**Let ACP own plan state.** Rejected because TUI, resume, fork, prompt assembly, and the exit tool need the same logged fact independently of ACP. ACP owns only the wire projection.
**Let one presentation transport own plan state.** Rejected because TUI, Web, resume, fork, prompt assembly, and the exit tool need the same logged fact independently of any one transport. Presentation adapters own only their projections.
**Split a capability-seam trio or put the state in the agent loop.** Rejected because plan mode has no swappable backend, while existing session, prompt, tool, command, and lifecycle seams already provide every required hook.
**Put flips in surface messages or store plans in files.** Rejected because the stance is a log-only fact and the tool argument already records the reviewable plan. Surface duplication spends model context, while a plan directory creates a second durable home.
**Filter tools by a per-plan name allowlist or a global policy stack.** Rejected because mutability is a property of each tool, including future and MCP tools, rather than a list that every plan deployment must maintain. Effects metadata can establish a shared policy only when a concrete consumer exists; until then plan mode is guidance, not a security boundary.
**Review through the approval seam or prose.** Rejected because a plan review is not a permission decision, needs the exact artifact and corrective free text, and must have a logged tool call as its structured transition. The user-interaction seam supplies that contract.
## Verification
- Package tests retain boundary ordering, retry, append-failure, HMR disposal, prompt assembly, stable native and Code Mode schemas, review outcomes, and invariant coverage through the boolean service.
- Command tests cover bare `/plan`, `/plan <message>`, active `/plan off`, pending-entry cancellation, inactive idempotence, absence of `/mode` and `/review`, and effect-scoped removal.
- ACP tests cover fixed advertisement, both ids, unknown-id rejection, optimistic updates, committed exits, and load replay.
- The keyless TUI scenarios enter through `/plan <message>`, leave through `/plan off`, and prove that each committed `plan/mode` precedes the request header it changes, the entry message is logged under plan guidance, and the post-exit request omits that guidance.
- The complete `exit_plan_mode` review arc is package-tested but has no assembled-application snapshot after the interactive ACP scenarios were retired; current keyless TUI scenarios cover command entry and direct exit only.
## Consequences
The implementation has one vocabulary for one shipped feature. Adding another collaboration stance is now an explicit design decision instead of a config entry, while ACP clients continue to see their standard mode picker. The migration intentionally rejects old `mode/set` logs and old `modes.plan.section` configuration under the repository's pre-release format policy.
The implementation has one vocabulary for one shipped feature. Adding another collaboration stance is an explicit design decision instead of a config entry, and automation clients do not acquire human mode controls through ACP. The migration intentionally rejects old `mode/set` logs and old `modes.plan.section` configuration under the repository's pre-release format policy.
Plan state remains reconstructable and tool schemas remain stable, but an idle pending selection is lost if the process exits before the next boundary. Entering or leaving plan mode changes the prompt from order 50 onward, and a model that ignores the guidance can still mutate unless the deployment independently configures sandbox, approval, or filesystem policy.

View File

@@ -1,4 +1,4 @@
# Agent Note: 将具名会话模式收敛为 plan mode
# Agent Note: plan 专用协作状态
Status: implemented
@@ -8,7 +8,9 @@ Status: implemented
产品只交付了 `plan`,首个 plan mode 实现却引入了通用的具名模式注册表。`ModeConfig.modes`、定义名称校验、`ctx.modes.list()`、已退役定义的回退逻辑,以及测试中合成的 `review` 模式,都只为支持假想中的未来协作模式而存在。plan 引导、`/plan` 和 `exit_plan_mode` 这些生产专用行为仍位于同一个包(package)内,因此通用 API 并未将可复用机制与 plan 策略隔离开来。
「mode」一词还横跨互不相关的领域。沙箱模式是由 `ctx.sandboxPolicy` 拥有、以 `sandbox/mode` 记录日志的强制执行策略;plan mode 则是一种协作方式,会贡献引导内容和经评审的退出路径。若把两者都视为同一个具名模式抽象的实例,就会掩盖二者各自独立的归属关系。ACP(Agent Client Protocol)协议恰好暴露了通用模式选择器,但这只是适配器词汇,并不能证明 harness 需要通用模式领域。
「mode」一词还横跨互不相关的领域。沙箱模式是由 `ctx.sandboxPolicy` 拥有、以 `sandbox/mode` 记录日志的强制执行策略;plan mode 则是一种协作方式,会贡献引导内容和经评审的退出路径。若把两者都视为同一个具名模式抽象的实例,就会掩盖二者各自独立的归属关系。传输协议的通用词汇并不能证明 harness 需要通用模式领域。
Plan mode 还需要持久协作状态、可评审的计划产物、显式人工决策边界,以及跨恢复与 fork 的请求重建。即使移除通用注册表和 ACP 交互投影,这些要求仍归 plan 功能所有。
## 决策
@@ -16,10 +18,22 @@ Plan mode 拥有一个 plan 专用产品包:位于 `packages/plan/plan-mode/`
配置严格为 `{ section: string }`。该包自行注册固定的 `plan:policy` 段、`/plan [message]`、精确匹配的 `/plan off` 主动退出形式,以及 `exit_plan_mode`。不带参数的 `/plan` 选择激活;其他非空参数则先选择激活,再通过 `agent.steer()` 发送去除首尾空白后的文本,使该文本在受影响的步骤中成为一条记录到日志的普通用户消息。`/plan off` 选择未激活,不产生模型输入,并可取消仍待在边界生效的进入选择。即使 plan mode 未激活,退出工具仍保持注册,以确保请求工具目录稳定。
ACP 保留协议层的 `default` 和 `plan` id。桥接层把这两个 id 映射到布尔服务,只公布这组固定选项,在适配器边界拒绝其他所有 id,并把已提交的 `plan/mode` 事件映射回 `current_mode_update`。协议仍保持通用性,但不会迫使产品领域也采用通用抽象。
面向人类的组合拥有 plan 选择与评审。本笔记最初把 ACP 协议级的 `default`/`plan` 选择器保留为布尔服务之上的适配器;[ACP 作为仅面向自动化的协议](2026-07-23-acp-automation-only-protocol.md)取代了那个线上投影,因此 ACP 组合现在既不挂载 plan mode,也不提供模式选择协议。
沙箱模式与审批策略仍是彼此独立的强制约束轴。Plan mode 既不读取也不写入二者;此次简化也没有为这些概念引入共享基类型、注册表或预设抽象。
### 边界与模型契约
`plan/mode` 仅记录到日志且不进入表层,因此恢复、fork 和压缩都能恢复该状态,无需实时镜像。spawn 出的 agent 初始处于未激活状态,因为创建时没有 plan 选项。待生效的用户选择会在提示词提交、普通 continuation 或请求恢复重试时,于受影响的请求组装前写入日志;持久追加失败会让意图保持待定,留到后续边界处理。
激活状态在提示词顺序 50 处贡献部署提供的区段。未激活状态不贡献区段,但 `exit_plan_mode` 在两种状态下都保持注册,因此状态转换会改变已记录的请求头,却不改变原生工具 schema 或 Code Mode SDK。用户发起的转换只会在上一条请求头描述相反状态时追加一条来源为插件的通知;第一次请求前的选择或最终状态未变化的选择不会追加通知,经批准的工具退出则依赖其工具结果,不再追加第二条通知。
### 经评审的退出
`exit_plan_mode` 要求调用方 agent 处于激活的 plan mode,并提交一份非空、以标题开头的 markdown 计划。用户交互问题将这份原样计划作为详情,并提供 `Approve`、`Keep planning` 和自由文本反馈。仅当唯一选择为 `Approve` 且没有自定义文本时才视为同意;其他所有回答都会留在 plan mode,并向模型返回纠正性反馈。经批准的退出会成为一项静默的待生效选择,使 plan 引导在当前工具批次的剩余部分继续有效,并在下一次请求前移除。
工具将提交的计划渲染为 generic 卡片,标题取自第一个标题。用户交互提供方缺失或失败、评审失败,或评审待定期间插件被 dispose,都会失败关闭,并保留手动 `/plan off` 作为人类退出路径。
## 删除的接口
- 任意定义映射、模式名正则表达式、保留名称规则以及逐定义命令循环。
@@ -31,17 +45,27 @@ ACP 保留协议层的 `default` 和 `plan` id。桥接层把这两个 id 映射
**保留私有的通用注册表,目前只暴露 plan。** 不予采纳,因为没有第二个生产消费方时,仍需维护和测试未使用的名称与配置机制。未来若出现另一种协作状态,可以从两个具体案例出发建立合适的共享 seam。
**将沙箱模式折叠进同一服务。** 不予采纳,因为协作引导与执行约束有不同的归属方、生命周期语义和消费方。二者的英文名称都含「mode」,不代表存在领域关系。
**将沙箱或审批策略折叠进 plan 状态。** 不予采纳,因为协作引导、执行约束和权限决策有不同的归属方、生命周期语义和消费方。由 mode 拥有的沙箱上限还会让用户显式选择沙箱看似成功,实际却被静默忽略。
**让 ACP 拥有 plan 状态。** 不予采纳,因为 TUI、恢复、fork、提示词组装和退出工具都需要在 ACP 之外独立使用同一项已记录事实。ACP 只拥有协议投影。
**让一种呈现传输拥有 plan 状态。** 不予采纳,因为 TUI、Web、恢复、fork、提示词组装和退出工具都需要独立于任何单一传输使用同一项已记录事实。呈现适配器只拥有各自的投影。
**拆成能力 seam 三包,或把状态放进 agent loop。** 不予采纳,因为 plan mode 没有可替换后端,而现有的会话、提示词、工具、命令和生命周期 seam 已经提供所需的全部钩子。
**将状态切换写入表层消息,或把计划存入文件。** 不予采纳,因为协作状态是仅日志事实,工具参数已经记录了可评审的计划。重复写入表层会消耗模型上下文,而计划目录会形成第二个持久归属。
**按 plan 专用名称允许列表或全局策略栈筛选工具。** 不予采纳,因为可变性是每个工具自身的属性,包括未来工具和 MCP 工具,而不应由每个 plan 部署维护一份列表。只有出现具体消费方后,effects 元数据才能建立共享策略;在此之前,plan mode 是引导机制,不是安全边界。
**通过审批 seam 或普通文本完成评审。** 不予采纳,因为计划评审不是权限决策,需要精确的计划产物和纠正性自由文本,而且必须以已记录的工具调用作为结构化转换。用户交互 seam 提供了这项契约。
## 验证
- 包测试通过布尔服务继续覆盖边界顺序、重试、追加失败、HMR(热模块替换)资源释放、提示词组装、稳定的原生 schema 与 Code Mode schema、评审结果和不变式。
- 命令测试覆盖不带参数的 `/plan`、`/plan <message>`、激活状态下的 `/plan off`、取消待生效的进入选择、未激活状态下的幂等性、不存在 `/mode` 和 `/review`,以及随 effect 作用域移除。
- ACP 测试覆盖固定模式列表公布、两个 id、未知 id 拒绝、乐观更新、已提交退出和加载回放。
- 无密钥 TUI 场景通过 `/plan <message>` 进入、通过 `/plan off` 退出,并证明每个已提交的 `plan/mode` 都先于其所改变的请求头,进入消息在 plan 引导下记录到日志,且退出后的请求不含该引导。
- 完整的 `exit_plan_mode` 评审流程有包测试,但交互式 ACP 场景退役后没有组装应用快照;当前无密钥 TUI 场景只覆盖命令进入和直接退出。
## 后果
该实现只用一套词汇描述一项已交付功能。若要添加另一种协作方式,必须显式作出设计决策,而不能只增加配置项;ACP 客户端仍可看到标准模式选择器。根据仓库的预发布格式策略,本次迁移有意拒绝旧的 `mode/set` 日志与 `modes.plan.section` 配置。
该实现只用一套词汇描述一项已交付功能。若要添加另一种协作方式,必须显式作出设计决策,而不能只增加配置项;自动化客户端不会通过 ACP 获得面向人类的模式控制。根据仓库的预发布格式策略,本次迁移有意拒绝旧的 `mode/set` 日志与 `modes.plan.section` 配置。
Plan 状态仍可重建,工具 schema 仍保持稳定,但如果进程在下一边界前退出,空闲状态下待生效的选择会丢失。进入或离开 plan mode 会改变提示词顺序 50 处及其后的内容;如果模型忽略引导,仍可能执行修改,除非部署另行配置沙箱、审批或文件系统策略。

View File

@@ -2,5 +2,5 @@
# 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
2026-07-22-tui-titles-from-session-title-service.md: b54b99647230255cf241415f94aa21b2630c44cd
2026-07-22-tui-titles-from-session-title-service.zh.md: 67cc3332f0694887d5af0d71997d140b74669f46
2026-07-22-tui-titles-from-session-title-service.md: 04355b9c426af423dec347997f3b8ac62483eb7f
2026-07-22-tui-titles-from-session-title-service.zh.md: 5fc783c5ca08baba12a60f2aa6b5e286307aa9f0

View File

@@ -6,11 +6,11 @@ English | [中文](2026-07-22-tui-titles-from-session-title-service.zh.md)
## Problem
Two model-title implementations coexisted after the tui-staging line merged onto master. The TUI carried its own `autoTitle` feature: a fire-and-forget `ctx.llm.stream` call after the first user message that set the terminal window title via OSC 0, with a one-shot latch, its own prompt, its own 40-character cap, and its own resume re-derivation ([auto-title Agent Note](../feature/2026-07-21-tui-auto-pane-title.md), [default-on Agent Note](../feature/2026-07-21-tui-auto-title-default-on.md)). Master had meanwhile landed [log-backed session titles](../feature/2026-07-21-log-backed-session-titles.md): a `sessionTitle` capability whose accepted revisions are durable `session/title` events, with a deterministic fallback and optional model providers. The TUI already consumed `session/title` for its header subtitle and window title, so a session could be titled twice by different strategies, and the TUI's process-local title was invisible to every other consumer (ACP, resume listings, forks).
A per-session title makes terminal panes and tabs distinguishable, but a TUI-local model call would create a second title pipeline beside [log-backed session titles](../feature/2026-07-21-log-backed-session-titles.md). The local path needs its own prompt, cap, one-shot latch, resume derivation, cancellation, and failure fallback, while its process-local result remains invisible to session listings, forks, Web consumers, and replay. If both paths run, one session can also be titled twice by different strategies.
## Decision
The TUI-local generation is removed; the session-title service is the one title source. `TuiConfig.autoTitle`, the latch, the abort controller, the title prompt, and `titleLine` are gone from `dsh-tui`. The terminal rename stays: the TUI folds the latest logged title on mount (`foldSessionTitle`), renders it as the banner subtitle, and sets the terminal window title to `<session title> — <configured title>` on every accepted `session/title` event — including resumed sessions, whose titles now replay from the log instead of being re-generated.
The session-title service is the one title source. The TUI contains no `autoTitle` config, title-model request, latch, abort controller, prompt, or output cap. It folds the latest logged title on mount (`foldSessionTitle`), renders it as the banner subtitle, and calls `runtime.terminal.setTitle` with `<session title> — <configured title>` on every accepted `session/title` event. The same terminal-safe OSC 0 path handles the configured fallback title, resumed sessions, and live revisions without renaming tmux windows or adding another terminal-control surface.
Model-made titles are a composition choice: `examples/tui-agent/cordis.yml` (and the scripted PTY fixture) mount `@deepseek-ai/dsh-session-title-first-message-llm`, which inherits the main request's route and replaces the spine's deterministic fallback with a short model summary. Deployments without the provider keep the fallback title from `dsh-agent-spine-demo`'s bundled `SessionTitleService`.
@@ -20,6 +20,16 @@ Model-made titles are a composition choice: `examples/tui-agent/cordis.yml` (and
**Port auto-title's prompt and cap into the service as a third provider.** The first-message-llm provider already exists with the same cadence, a reviewed prompt contract, durable request records, and supersession fencing; a second near-identical provider would be pure duplication.
**Use only a truncated first prompt or only a model title.** A deterministic fallback provides an immediate, free title, while an optional model provider improves quality without delaying the main turn. Forcing either strategy removes that deployment choice.
**Make model titles a TUI default or block the first turn for them.** The cost and route belong to composition, and auxiliary title latency must stay off the interaction critical path. The TUI consumes accepted state instead of owning generation policy.
**Rename a tmux window or use a separate terminal escape.** Rejected because the existing terminal adapter's OSC 0 path labels the pane or tab without acquiring tmux ownership or adding a second control API.
## Verification
TUI tests pin restored and live `session/title` consumption, terminal-safe title rendering, the configured fallback, and the absence of a TUI-owned model path. The keyless PTY smoke boots the real composition, accepts a logged provider title, and observes the resulting terminal title. The [log-backed title decision](../feature/2026-07-21-log-backed-session-titles.md) owns provider, persistence, resume, fork, cancellation, and stale-completion coverage.
## Consequences
One title pipeline: durable, replayable, visible to every consumer, and fenced against stale completions by the service. The TUI sheds ~90 lines and its `llm`-streaming path. The cost is that a title now requires the provider plugin in the composition for model quality — a leaf choice, not a TUI default — and the terminal title changes shape from the bare model summary to the suffixed `<title> — <product>` form the log-backed path always used. The superseded auto-title Agent Notes carry pointers here.
One title pipeline is durable, replayable, visible to every consumer, and fenced against stale completions by the service. The TUI has no `llm`-streaming title path. Model quality requires a provider plugin in the composition, while deployments without one keep the deterministic fallback; the terminal title consistently uses the suffixed `<title> — <product>` shape.

View File

@@ -6,11 +6,11 @@ Status: implemented
## 问题
tui-staging 分支合入 master 后,两套模型标题实现并存。TUI 自带 `autoTitle` 特性:在首条用户消息后发起一次 fire-and-forget 的 `ctx.llm.stream` 调用,通过 OSC 0 设置终端窗口标题,带有一次性闩锁、自己的提示词、自己的 40 字符截断和自己的恢复重推导([auto-title Agent Note](../feature/2026-07-21-tui-auto-pane-title.md)、[default-on Agent Note](../feature/2026-07-21-tui-auto-title-default-on.md))。而 master 已落地[日志承载的会话标题](../feature/2026-07-21-log-backed-session-titles.md):一个 `sessionTitle` 能力,其被接受的修订是持久的 `session/title` 事件,带确定性回退和可选的模型 provider。TUI 已经消费 `session/title` 作为横幅副标题和窗口标题,于是一个会话可能被两种策略各标题一次,且 TUI 的进程本地标题对其他所有消费者(ACP、恢复列表、fork)不可见。
每会话标题让终端窗格和标签页易于区分,但 TUI 本地模型调用会在[日志承载的会话标题](../feature/2026-07-21-log-backed-session-titles.md)旁形成第二条标题管线。本地路径需要自己的提示词、截断上限、一次性闩锁、恢复推导、取消和失败回退,而其进程本地结果仍对会话列表、fork、Web 消费方和回放不可见。若两条路径同时运行,同一会话还可能被不同策略命名两次。
## 决策
移除 TUI 本地生成;session-title 服务是唯一的标题来源。`TuiConfig.autoTitle`、闩锁、abort controller、标题提示词和 `titleLine` 全部从 `dsh-tui` 删除。终端重命名保留:TUI 在挂载时折叠最新的已记录标题(`foldSessionTitle`),将其渲染为横幅副标题,并在每个被接受的 `session/title` 事件上把终端窗口标题设为 `<会话标题> — <配置标题>` —— 包括恢复的会话,其标题现在从日志回放而不是重新生成。
session-title 服务是唯一的标题来源。TUI 不包含 `autoTitle` 配置、标题模型请求、闩锁、abort controller、提示词或输出上限。TUI 在挂载时折叠最新的已记录标题(`foldSessionTitle`),将其渲染为横幅副标题,并在每个被接受的 `session/title` 事件上调用 `runtime.terminal.setTitle`,传入 `<session title> — <configured title>`。同一条终端安全的 OSC 0 路径会处理配置的回退标题、恢复的会话和实时修订,既不重命名 tmux 窗口,也不增加另一套终端控制接口。
模型生成的标题是组合选择:`examples/tui-agent/cordis.yml`(以及脚本化 PTY fixture)挂载 `@deepseek-ai/dsh-session-title-first-message-llm`,它继承主请求的确切路由,用简短的模型摘要替换 spine 的确定性回退。未挂载该 provider 的部署保留 `dsh-agent-spine-demo` 内置 `SessionTitleService` 的回退标题。
@@ -20,6 +20,16 @@ tui-staging 分支合入 master 后,两套模型标题实现并存。TUI 自
**把 auto-title 的提示词和截断移植为服务的第三个 provider。** first-message-llm provider 已经存在,节奏相同,且有经过评审的提示词契约、持久的请求记录和替换围栏;再造一个近乎相同的 provider 纯属重复。
**只使用截断后的首条提示词,或只使用模型标题。** 确定性回退可以立即且免费地提供标题,而可选模型 provider 可以提升质量,不会延迟主轮次。强制采用任一种策略都会移除这项部署选择。
**让模型标题成为 TUI 默认行为,或为此阻塞第一个轮次。** 成本与路由归组合所有,辅助标题的延迟不得进入交互关键路径。TUI 只消费已接受的状态,不拥有生成策略。
**重命名 tmux 窗口,或使用另一种终端转义序列。** 不予采纳,因为现有终端适配器的 OSC 0 路径可以标记窗格或标签页,无需取得 tmux 归属,也无需增加第二套控制 API。
## 验证
TUI 测试锁定恢复后和实时的 `session/title` 消费、终端安全的标题渲染、配置的回退标题,以及不存在 TUI 自有模型路径。无密钥 PTY 冒烟测试启动真实组合,接收已记录的 provider 标题,并观察由此产生的终端标题。[日志承载标题决策](../feature/2026-07-21-log-backed-session-titles.md)拥有 provider、持久化、恢复、fork、取消和陈旧完成结果的覆盖。
## 影响
标题管线归一:持久、可回放、对所有消费者可见,并由服务对过期完成设防。TUI 削减约 90 行及其 `llm` 流式路径。代价是模型质量的标题现在需要在组合中挂载 provider 插件 —— 这是叶配置选择,不是 TUI 默认值 —— 且终端标题形状从裸模型摘要变为日志路径一贯使用的 `<标题> — <产品>` 后缀形式。被取代的 auto-title Agent Note 携带指向本文的指针。
唯一的标题管线持久、可回放、对所有消费方可见,并由服务防止陈旧完成结果生效。TUI 不再有 `llm` 流式标题路径。若要提升模型标题质量,组合中必须挂载 provider 插件;未挂载的部署保留确定性回退。终端标题始终采用 `<title> — <product>` 后缀形式。

View File

@@ -2,5 +2,5 @@
# 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
2026-07-04-fold-stdio-ui-helper.md: 01165df19942e8e0adf84ede3a371f53ad12e464
2026-07-04-fold-stdio-ui-helper.zh.md: 37b316283e96d7640e1e7af5fdbb05ab487aa6fa
2026-07-23-acp-automation-only-protocol.md: 0fe2fc27a963d21e8a24c1682359ab3bc9e7af48
2026-07-23-acp-automation-only-protocol.zh.md: 0a471f0bf1b12e835660cdce2d2a2acd761e1b89

View File

@@ -0,0 +1,53 @@
# Agent Note: ACP as an automation-only protocol
Status: implemented
English | [中文](2026-07-23-acp-automation-only-protocol.zh.md)
## Problem
The ACP bridge had become a second interactive product UI. It translated durable events into editor cards, terminal metadata, diffs, plans, titles, reasoning, commands, modes, model and permission pickers, session navigation, and human elicitation. Those responsibilities duplicated the TUI and the Web client while coupling an automation transport to UI services, persistence queries, presentation policy, and editor-specific conventions.
ACP still has one useful role: another agent or automated controller can start a harness process, create an isolated session, send text, receive the committed answer, cancel work, and answer a permission request. The out-of-process ACP subagent backend depends on that standard protocol boundary.
The snapshot suite complicates removal. Most ACP scenarios exercise the assembled agent backend rather than ACP presentation, so deleting the suite with the editor bridge would discard broad keyless behavioral coverage.
## Decision
`@deepseek-ai/dsh-acp` is an automation transport under [`packages/acp/acp`](../../../../packages/acp/acp/README.md), outside the `ui` package group. Its public protocol is intentionally small: version negotiation, fresh text sessions with one in-flight prompt each, committed assistant text updates, per-session cancellation, concurrent sessions, and connection-owned teardown. Prompts carry the spec-required baseline only — text plus resource links flattened to bracketed textual references; the bridge rejects additional directories, MCP servers, beyond-baseline prompt content (image, audio, embedded resources), empty prompts, unknown sessions, and overlapping prompts.
The bridge emits only committed `assistant/message` text. Reasoning, raw chunks, tool activity, todos, plans, titles, retry markers, terminal metadata, diffs, locations, and resource links remain in the durable session log or in UI-specific transports. It does not provide session load/list/delete, commands, modes, configuration selectors, model switching, plan review, or human elicitation.
One-shot `session/request_permission` remains. It is a machine policy channel for bridge-owned agents, not a human approval UI: the answerer accepts only an exact agent object in the bridge's live session map, delegates foreign or call-less requests, and maps failed RPCs to the fail-closed unavailable outcome. The client chooses allow once, reject once, or cancel, and the bridge never turns that response into a durable grant. Asking policy stays in the approval seam and its producers; [`dsh-subagent-acp`](../../../../packages/subagent/subagent-acp/README.md) uses this channel programmatically.
The app composition contains the agent spine, persistence, checkpoint policy, and ACP transport. It does not mount command, session-query, session-reference, plan-mode, permission-picker, or user-interaction services for ACP. SDK scaffolding likewise treats `ask_user_question` as TUI-only.
The transport programs interface-level agent, session, and approval services rather than the concrete agent loop. Tool execution stays inside the harness; ACP never delegates shell execution to an editor. stdout carries framed JSON-RPC only, so the app mounts no stdout logger and the bridge does not monkey-patch process output.
Disconnect and plugin disposal share one memoized quiescence boundary. Both successful and failed transport closure settle pending prompts as cancelled, dispose every bridge-owned agent, and await loop and session cleanup. A create that loses the close race disposes its unpublished handle.
## Snapshot boundary
The ACP snapshot suite still boots the assembled ACP example and retains scenarios that pin backend behavior. Only scenarios driven through deleted UI methods leave the suite; semantic-checkpoint recovery runs through the headless `stream-json` example because ACP no longer loads sessions.
Protocol and lifecycle tests pin stop-reason and prompt codecs, version negotiation, fresh-session creation, text and resource-link flattening, rejection of empty or unsupported prompts, exact-agent permission ownership, multi-session isolation, prompt settlement, per-session cancellation, failed transport closure, ACP-only reload cleanup, and teardown quiescence. Built and real-stdio smokes reject stray stdout. The `session/new` branch that loses a real stdio close race remains coverage-exempt because the in-memory transport cannot reproduce that ordering; it disposes the unpublished handle, while the surrounding disposal tests pin the no-orphan invariant.
## Alternatives considered
**Keep ACP as an editor UI until Web reaches parity.** Rejected because it leaves two interactive contracts to evolve and keeps editor conventions in the automation boundary.
**Keep the earlier editor bridge behind disciplined seams.** Rejected even though that bridge correctly used interface services, tool-owned render intents, approval and user-interaction answerers, harness-owned execution, and a stdout-pure composition. Its terminal cards were capability-gated, display-only Zed `_meta` projections with a text fallback rather than ACP `terminal/create`, so shell execution never left the harness. The projection derived each display terminal id from the stable per-call id to prevent collisions and recovered exit code or signal from the rendered status markers because the pure result presenter received content blocks rather than a structured exit; marker round-trip tests and an explicit no-capability `console` fallback test pinned both contracts. Those boundaries were coherent but could not make editor cards, session navigation, configuration pickers, and human elicitation belong in an automation protocol.
**Replace ACP with a private subagent RPC.** Rejected because ACP already supplies a typed, interoperable process protocol and is used by the out-of-process subagent backend.
**Remove machine permission requests with the other interaction features.** Rejected because an automated parent must answer a child agent's one-shot policy decision; this is control flow between agents, not presentation.
**Delete the ACP snapshot suite or migrate every scenario in this change.** Rejected because most scenarios test the backend and remain valuable, while a full harness migration is an independent testing change. Only scenarios whose driver was a deleted UI method leave this suite.
## Consequences
ACP has a narrow contract suitable for agents and automation, while TUI and Web own human interaction and presentation. The package has fewer injected services, dependencies, protocol branches, and lifecycle states, and it no longer claims compatibility as a general editor front door.
Automation clients receive complete committed text rather than token deltas or structured tool UI. They inspect durable logs or another API when they need reasoning, tool traces, titles, or richer state. Fresh-session-only operation also means callers that need durable browsing or resume use a host API rather than ACP.
Backend snapshot coverage therefore remains transport-coupled to ACP even though that transport is incidental to the behavior under test.

View File

@@ -0,0 +1,53 @@
# Agent Note:ACP 作为仅面向自动化的协议
Status: implemented
[English](2026-07-23-acp-automation-only-protocol.md) | 中文
## 问题
ACP(Agent Client Protocol)桥接层已经变成第二套交互式产品 UI。它将持久事件转换为编辑器卡片、终端元数据、diff、计划、标题、推理、命令、模式、模型和权限选择器、会话导航以及面向人类的询问。这些职责与 TUI 和 Web 客户端重复,同时将自动化传输层与 UI 服务、持久化查询、展示策略和编辑器特定约定耦合在一起。
ACP 仍有一个有用的职责:另一个 agent(智能体)或自动化控制器可以启动 harness 进程、创建隔离会话、发送文本、接收已提交的回答、取消工作并回答权限请求。跨进程 ACP subagent 后端依赖这个标准协议边界。
快照套件使移除工作更复杂。大多数 ACP 场景测试的是组装后的 agent 后端,而不是 ACP 展示层;如果随编辑器桥接层一起删除整个套件,就会丢失大量无密钥行为覆盖。
## 决策
`@deepseek-ai/dsh-acp` 是位于 [`packages/acp/acp`](../../../../packages/acp/acp/README.md) 下、独立于 `ui` 包组的自动化传输层。其公开协议特意保持精简:版本协商、全新文本会话(每个会话最多允许一个进行中的提示词)、已提交的助手文本更新、按会话取消、并发会话,以及由连接负责的资源清理。提示词只承载规范要求的基线内容——文本,加上被展平为方括号文本引用的资源链接;桥接层会拒绝附加目录、MCP 服务器、超出基线的提示词内容(图片、音频、内嵌资源)、空提示词、未知会话和重叠提示词。
桥接层只发出已提交的 `assistant/message` 文本。推理、原始分片、工具活动、待办事项、计划、标题、重试标记、终端元数据、diff、位置和资源链接仍保留在持久会话日志或 UI 专用传输层中。它不提供会话加载、列出与删除、命令、模式、配置选择器、模型切换、plan 评审或面向人类的询问。
保留一次性 `session/request_permission`。它是为桥接层拥有的 agent 提供的机器策略通道,而不是面向人类的审批 UI:应答者只接受桥接层当前会话映射中的精确 agent 对象;外部请求或缺少调用标识的请求会继续委派;RPC 失败则映射为失败关闭的 `unavailable` 结果。客户端可选择允许一次、拒绝一次或取消,桥接层绝不会将该响应转换为持久授权。询问策略仍归审批 seam 及其生产者所有;[`dsh-subagent-acp`](../../../../packages/subagent/subagent-acp/README.md) 会以程序化方式使用该通道。
应用组装包含 agent 主干、持久化、检查点策略和 ACP 传输层。它不会为 ACP 挂载命令、会话查询、会话引用、plan mode、权限选择器或用户交互服务。SDK 脚手架同样将 `ask_user_question` 视为 TUI 专属功能。
传输层调用 agent、会话和审批的接口服务,而不依赖具体的 agent loop。工具执行仍留在 harness 内;ACP 绝不会把 shell 执行委派给编辑器。stdout 只承载分帧 JSON-RPC,因此 app 不挂载 stdout logger,桥接层也不会 monkey-patch 进程输出。
断开连接与插件 dispose(资源释放)共享同一个经记忆化处理的静止边界。传输关闭无论成功还是失败,都会将待处理提示词以已取消状态结算,dispose 每个由桥接层拥有的 agent,并等待循环和会话清理完成。创建流程如果在与关闭的竞态中落败,就会 dispose 其尚未发布的 handle。
## 快照边界
ACP 快照套件仍会启动组装后的 ACP 示例,并保留用于锁定后端行为的场景。从该套件移出的只有通过已删除的 UI 方法驱动的场景;由于 ACP 不再加载会话,语义检查点恢复通过 headless `stream-json` 示例执行。
协议与生命周期测试会锁定停止原因编解码器和提示词编解码器、版本协商、新会话创建、文本与资源链接展平、拒绝空提示词或不受支持的提示词、精确 agent 权限归属、多会话隔离、提示词结算、按会话取消、传输关闭失败、ACP 专属重载清理,以及拆卸完全停稳。构建产物冒烟测试与真实 stdio 冒烟测试会拒绝混入 stdout 的额外输出。`session/new` 中在真实 stdio 关闭竞态中落败的分支仍属于覆盖豁免,因为内存传输层无法复现这一顺序;该分支会 dispose 尚未发布的 handle,而周边 dispose 测试会锁定无遗留资源不变式。
## 考虑过的替代方案
**在 Web 达到同等能力前,继续将 ACP 作为编辑器 UI。** 不予采用,因为这会留下两套需要演进的交互契约,并使编辑器约定继续存在于自动化边界中。
**通过严格的 seam 保留早期编辑器桥接层。** 不予采用,尽管该桥接层正确使用了接口服务、工具自有的 render intent、审批与用户交互应答者、harness 自有执行,以及保持 stdout 纯净的组装。其终端卡片是经过能力门控、仅用于展示的 Zed `_meta` 投影,并提供文本回退,而非使用 ACP `terminal/create`,因此 shell 执行从未离开 harness。该投影从稳定的逐调用 id 派生每个展示用终端 id,以避免冲突;由于纯结果展示器接收的是内容块,而不是结构化退出信息,它会从渲染后的状态标记中恢复退出码或信号。标记往返测试和显式的无能力 `console` 回退测试锁定了这两项契约。这些边界保持一致,却无法让编辑器卡片、会话导航、配置选择器和面向人类的询问成为自动化协议应有的职责。
**用私有 subagent RPC 替换 ACP。** 不予采用,因为 ACP 已经提供类型化、可互操作的进程协议,并由跨进程 subagent 后端使用。
**随其他交互功能一起移除机器权限请求。** 不予采用,因为自动化父 agent 必须回答子 agent 的一次性策略决策;这是 agent 之间的控制流,而不是展示层。
**删除 ACP 快照套件,或在本次变更中迁移每个场景。** 不予采用,因为大多数场景测试后端且仍有价值,而完整的 harness 迁移是一项独立的测试变更。只有驱动脚本依赖已删除 UI 方法的场景才离开该套件。
## 结果
ACP 具有适合 agent 与自动化的精简契约,而 TUI 和 Web 拥有面向人类的交互与展示。该包注入的服务、依赖、协议分支和生命周期状态更少,也不再将自身定位为通用编辑器入口。
自动化客户端收到完整的已提交文本,而不是 token 增量或结构化工具 UI。当它们需要推理、工具跟踪信息、标题或更丰富的状态时,需要查看持久日志或其他 API。只支持全新会话也意味着,需要浏览持久会话或恢复会话的调用方必须使用 host API,而不是 ACP。
因此,后端快照覆盖仍与 ACP 传输层耦合,尽管对于受测行为而言,该传输层只是附带因素。