Merge remote-tracking branch 'origin/master' into worktree/acp-automation-protocol
# Conflicts: # .agents/notes/implemented/architecture/2026-06-14-session-persistence.md # .agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md # .agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md # .agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md # .agents/notes/implemented/feature/2026-06-14-acp-multi-session.md # .agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md # .agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml # .agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md # .agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.zh.md # .agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md # docs/architecture.i18n.yaml # docs/cookbook/extension-cookbook.i18n.yaml # docs/cookbook/extension-cookbook.md # docs/cookbook/extension-cookbook.zh.md # docs/core-data-structures/approval.md # docs/core-data-structures/user-interaction.md # docs/event-producer-consumer.md # docs/persistence-catalog.md # docs/testing.md # docs/tool-catalog.md # examples/acp-agent/tests/fixtures/live-mode-switching-2026-07-07.session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl # examples/acp-agent/tests/snapshots/permission-switching/session.jsonl # packages/goal/tool-goal/README.md # packages/ui/acp/README.md # packages/ui/acp/acp-feature-support.md # packages/ui/acp/src/index.ts # packages/ui/acp/tests/bridge.spec.ts # packages/ui/acp/tests/dispose.spec.ts # packages/ui/acp/tests/edges.spec.ts # packages/ui/acp/tests/turns.spec.ts
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# 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-06-16-typed-event-schemas.md: 97a7d0c3787eb5556696e25a6c8b1bb75642aba9
|
||||
2026-06-16-typed-event-schemas.zh.md: c19f67c6ff058d42293ff2b6346630fe91c54dec
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-06-16-typed-event-schemas.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The harness models its core vocabulary — content blocks, message sources, finish reasons, turn triggers, turn-end reasons, and session events — as **merge-extensible maps**: a TypeScript `interface` (e.g. `SessionEventMap`, `ContentBlockMap`) that plugins augment via declaration merging, with the public union derived as `Map[keyof Map]`. This is the repo's universal extension pattern, documented in [docs/architecture.md](../../../../docs/architecture.md) ("The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`") and relied on by the `defineTool` `InferArgs` DSL and the `assertNever` exhaustiveness convention.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Agent Note: 事件词汇的运行时 schema(Zod 与 merge-extensible-map 模式之辩)
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-06-16-typed-event-schemas.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
harness 将其核心词汇——内容块、消息来源、结束原因、轮次触发器、轮次结束原因与会话事件——建模为 **merge-extensible map**:一个 TypeScript `interface`(如 `SessionEventMap`、`ContentBlockMap`),插件通过声明合并对其扩展,公开联合类型则以 `Map[keyof Map]` 派生。这是本仓库的通用扩展模式,记录在 [docs/architecture.md](../../../../docs/architecture.md) 中(「The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`」),`defineTool` 的 `InferArgs` DSL 和 `assertNever` 穷举约定都依赖于它。
|
||||
|
||||
该模式**仅存在于编译期**。类型在运行时消失:没有 schema 对象可供校验传入值、解析不可信输入或在运行时枚举变体。[会话持久化契约](../../implemented/architecture/2026-06-14-session-persistence.md)暴露了两个后果:
|
||||
|
||||
1. **持久化将 `event.data` 视为不透明 JSON。** JSONL/SQLite 后端对每个事件逐字 `JSON.stringify`/`JSON.parse`;唯一的运行时守卫是 `isJsonValue`(往返可序列化性检查:拒绝 BigInt、函数、循环引用、非有限数等),而非结构校验。一个损坏但仍为合法 JSON 的事件数据(字段类型错误、字段缺失)会静默往返,只有在后续消费方的 `switch` 中才可能被捕获。
|
||||
2. **插件新增变体没有运行时契约。** 一个通过声明合并添加新 `SessionEventMap` 键的插件,在自身代码中获得了编译期类型,但没有任何机制校验它产出的值是否符合它所声明的形状——无论是在生产者处、持久化边界处还是重新加载时。
|
||||
|
||||
由此引出问题:事件词汇是否应迁移到 **Zod** 或其他运行时 schema 库,使持久化和插件边界拥有运行时 schema 而非被擦除的类型。
|
||||
|
||||
本 Agent Note(agent 决策记录)界定该问题的范围,不提出具体实现。
|
||||
|
||||
## 为什么这不是一个持久化层的改动
|
||||
|
||||
很容易把「用 Zod 做序列化」理解为对 `dsh-session-persistence-jsonl/src/format.ts` 的局部修改。但它不是,原因在于一个结构性事实:**插件无法对 Zod schema 进行声明合并。** 声明合并是 TypeScript 编译期机制;Zod schema 是运行时值。要用 Zod 校验事件,就需要一个**运行时注册表**,每个产出事件的包(package)向其贡献自己的 schema(如 `ctx.sessionEvents.register('compaction/marker', z.object({…}))`),每个消费方从中读取。这个注册表——而非持久化后端——将成为词汇的真源,取代 merge-extensible 接口。
|
||||
|
||||
因此,真正的提案是:**用运行时 schema 注册表替换编译期的 merge-extensible-map 模式,范围覆盖整个仓库。** 这是一次核心词汇的重新设计。
|
||||
|
||||
## 影响范围(已度量)
|
||||
|
||||
将事件/词汇表面迁移到运行时 schema,至少涉及:
|
||||
|
||||
- **六个 merge-extensible map**(约 370 行核心类型):`ContentBlockMap`、`MessageSourceMap`、`FinishReasonMap`(位于 `dsh-llm`);`TurnTriggerMap`、`TurnEndReasonMap`、`SessionEventMap`(位于 `dsh-session`)。
|
||||
- **约 10 处 `declare module` 扩展点**,分布在 `dsh-agent`、`dsh-agent-loop`、`dsh-bash`、`dsh-llm`、`dsh-session`、`dsh-session-persistence`、`dsh-system-prompt`、`dsh-tools` 各包中——每处都将从声明合并改为运行时 `register()` 调用。
|
||||
- **事件生产者**——agent loop(智能体循环)中 16 处 `session.append(...)` 调用——形状不变,但现在在边界处被校验。
|
||||
- **约 7 个 switch 消费方**,对这些联合类型进行分支:`deriveMessages` 与包自有的不变式 companion(`dsh-session`)、`BlockAssembler`(`dsh-llm`)、两个 LLM(大语言模型)适配器(`dsh-llm-deepseek`、`dsh-llm-pi-ai`)以及工具 schema 层(`dsh-tools`)。`assertNever` 对封闭联合类型的穷举 vs 对可扩展联合类型的 fall-through 约定(一条已记录的 lint 规则)需要重新考量——运行时变体在静态层面不可穷举。
|
||||
- **`defineTool` 的 `InferArgs` DSL**(`dsh-tools`),它从编译期 schema 规范派生出零类型转换的 `execute` 参数类型——这是当前方案的标杆用例。
|
||||
- **文档**:architecture.md(该模式被描述为基础性的)、[开发模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md),以及所有引用该模式的 Agent Note。
|
||||
|
||||
这是一次仓库级别的词汇重新设计,而非持久化的实现细节。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### A. 维持现状——merge-extensible 类型 + 持久化边界处 `isJsonValue`
|
||||
保留编译期模式。持久化继续使用不透明 JSON + 可序列化性守卫。插件通过声明合并扩展;事件*形状*的正确性由生产者负责,并由 TypeScript 在编译期保证。启用包自有的不变式 companion 后,它们会检查选定的跨记录关系,但不提供通用运行时形状 schema。
|
||||
|
||||
- **优点**:零变动;插件扩展只需一行 `interface` 增补,享有完整类型推断,无需运行时注册仪式;无新运行时依赖;`defineTool` DSL 与 `assertNever` 穷举继续工作。
|
||||
- **缺点**:持久化边界和插件 seam 处无运行时结构校验;格式错误但仍为合法 JSON 的数据被延迟捕获。
|
||||
|
||||
### B. 仅对头部/封闭形状做校验(schemastery),事件仍为不透明
|
||||
仅对那些已有手写类型守卫的真正封闭形状加以收紧——例如 JSONL 的 `HeaderLine` 守卫(`isHeaderLine`)——使用 **schemastery**(仓库现有的 schema 库,已用于每个插件的 `static Config`)。merge-extensible 事件联合类型保持不变。
|
||||
|
||||
- **优点**:改动小,契合现有约定(schemastery,而非新库);用声明式 schema 替换封闭形状上的手写守卫;无核心重新设计。
|
||||
- **缺点**:不解决事件数据校验问题;仅固定的元数据记录得到改善。
|
||||
|
||||
### C. 为整个词汇建立运行时 schema 注册表(Zod 或 schemastery)
|
||||
用运行时注册表替换 merge-extensible map,生产者向其贡献 schema,持久化/消费路径据此校验。
|
||||
|
||||
- **优点**:持久化边界和插件 seam 处获得真正的运行时校验;单一真源;可支撑通用工具(自动生成文档、模糊测试、协议格式检查)。
|
||||
- **缺点**:上述全部影响范围;**Zod 目前不是直接依赖**(仅作为 `@earendil-works/pi-ai` 的传递依赖),仓库选定的 schema 库是 **schemastery**——广泛引入 Zod 本身就是一个依赖决策;声明合并的易用性(一行插件扩展、完整推断)被运行时注册 + 手动类型接线取代;`assertNever` 穷举保证弱化(运行时变体在静态层面不可穷举)。
|
||||
|
||||
## 提案
|
||||
|
||||
推迟。如果需要在持久化边界做运行时校验,**方案 B**(对封闭的头部和元数据形状使用 schemastery)是现有约定下的适度步骤。**方案 C** 是一个架构决策,需要自己的实现 Agent Note,其中包括 Zod 与 schemastery 之间的选择。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 方案 C 只能通过自己的实现 Agent Note 推进,绝不能作为持久化的附带改动。
|
||||
- 如果采纳方案 B,封闭的头部/元数据形状(JSONL 的 `isHeaderLine` 守卫及同类)改用 schemastery 校验,替代手写守卫,merge-extensible map 保持不动。
|
||||
|
||||
## 风险
|
||||
|
||||
- 推迟意味着事件 `data` 在持久化边界处仍无结构校验:格式错误但仍为合法 JSON 的数据被延迟捕获,由消费方的 `switch` 兜底——这是现状的代价,有意接受。
|
||||
- 如果方案 C 最终被采纳,易用性的损失是真实的:一行声明合并变为运行时注册加手动类型接线,`assertNever` 的静态穷举保证弱化。
|
||||
|
||||
## 待解问题
|
||||
|
||||
- 如果采用注册表,库选 **schemastery**(已在仓库中,已作为配置 schema 库)还是 **Zod**(生态更丰富,目前仅为传递依赖)?同时维护两个 schema 库本身就是一种成本。
|
||||
- 能否采用混合方案:保留编译期推断(使 `defineTool` 和插件开发体验不受影响),同时为每个变体添加*可选*的运行时 schema,仅在持久化/协议边界校验,而非每次进程内 append 都校验?
|
||||
- `ctx.invariants` 服务启用后是否已覆盖了足够多的运行时形状缺口,使得边界校验仅在面对真正不可信输入(重新加载外部修改过的日志)时才有必要?
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-24-separate-context-injection-from-turn-execution.md: 652c3d410ab625d91a828f854bce302adcb0c9e0
|
||||
2026-07-24-separate-context-injection-from-turn-execution.zh.md: 1064e7a869ab9ea46c0145eb010119894a03aacf
|
||||
@@ -0,0 +1,75 @@
|
||||
# Agent Note: Separate context injection from turn execution
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-07-24-separate-context-injection-from-turn-execution.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The agent API currently represents supplementary model-facing input in three overlapping ways: callers attach `HookContext[]` through `SendOptions.contexts`, interception and tool hooks return `additionalContexts`, and plugins call `agent.inject()`. These paths eventually write context into the same model history, but they carry different placement, metadata, admission, queue, and turn-lifecycle rules.
|
||||
|
||||
Atomic attachment to an inbox message forces the loop to preserve context through prompt admission, steering conversion, cancellation, and terminal discard. `prompt-prefix` placement then combines context and the direct prompt into one event, requiring a model-hidden envelope so transcript consumers can recover what the user actually wrote. The result makes outbox entries, session projection, and UI replay responsible for a distinction that belongs to the producer.
|
||||
|
||||
Idle `inject()` exposes a second mismatch. Injection does not request model execution, yet the current implementation opens and closes a zero-step `injection` turn solely to satisfy the turn-enclosure invariant and obtain a durability checkpoint. A turn therefore sometimes means “run the agent loop” and sometimes means “persist context without running it.”
|
||||
|
||||
`HookContext` also names its producer rather than its role. The value may come from a native plugin, a hook bridge, prompt admission, or tool post-processing. Its stable meaning is simply additional model-facing context with provenance.
|
||||
|
||||
## Proposal
|
||||
|
||||
Make `inject()` the only caller-facing operation for adding supplementary model-facing input, and define a turn exclusively as one execution of the model loop.
|
||||
|
||||
Remove `SendOptions.contexts`. A caller that owns context delivers it with `inject()` and independently submits the direct message with `send()` or `steer()`. Rename `HookContext` to `AdditionalContext`; retain only `content` and `source`, and remove placement and model-hidden metadata from this shared shape.
|
||||
|
||||
Prompt and tool extension points may still return `additionalContexts`. These values are outputs of the extension point, not attachments captured from a caller's inbox item. Prompt admission runs before `run()` opens a turn. An allowed prompt enters the outbox together with its returned additional contexts; a blocked prompt writes neither and opens no turn. Tool-produced additional contexts enter the same outbox after the corresponding tool results.
|
||||
|
||||
Every additional context becomes an independent `user/message` whose `source` records provenance. Remove `context/message`, prompt-prefix placement, the stable request delimiter, and the prompt envelope. Transcript and UI consumers distinguish direct user messages from injected context by `source`, not by recovering a hidden direct-prompt field from combined model content.
|
||||
|
||||
## Injection lifecycle
|
||||
|
||||
When a turn is open, `inject()` stages the context in the loop outbox. The loop drains the outbox at a safe step boundary, preserving tool protocol adjacency: a context accepted during an assistant tool-call batch appears only after that batch's complete ordered results. Taking the outbox as a whole makes steering and injected context accepted for one boundary visible to the same following request.
|
||||
|
||||
When no turn is open, `inject()` appends its `user/message` immediately and starts a session flush. It does not increment turn numbering, emit `turn/start` or `turn/end`, change agent status, or run the model. The synchronous API still returns before the asynchronous flush settles; `whenIdle()` and agent disposal include outstanding idle-injection flushes in their quiescence boundary.
|
||||
|
||||
A failed idle flush has no legitimate turn or step coordinates. It is reported through logging or a persistence-owned error surface, not by inventing an `agent/error` payload for a nonexistent turn. The in-memory event remains accepted and a later flush may retry persistence.
|
||||
|
||||
The session invariant therefore permits `user/message` between turns while continuing to require turn enclosure for execution events, steering, assistant output, tools, and package-added events by default. Persistence, recovery, resume, fork, and compaction code must treat a valid out-of-turn `user/message` as committed session history rather than an interrupted or discardable turn tail.
|
||||
|
||||
## Extension and caller semantics
|
||||
|
||||
`PromptDecision.content` continues to replace only the direct prompt. `PromptDecision.additionalContexts` and tool-result `additionalContexts` retain FIFO order and individual provenance, but no longer select placement. A waterfall listener that delegates with `next()` must preserve downstream prompt content and additional contexts unless it intentionally returns replacements.
|
||||
|
||||
Caller-driven injection and hook-produced additional context deliberately have different admission ownership. A hook's additional contexts materialize only after that hook allows the prompt or tool result. A caller that invokes `inject(context)` and then `send(prompt)` has already committed context independently; if prompt admission later blocks the prompt, the injected context remains in history. Callers requiring all-or-nothing domain behavior must perform their own preparation before either operation or expose a domain-specific admission seam.
|
||||
|
||||
Cross-session references follow the ordinary composition: the host prepares the snapshot, injects it with session-reference provenance, then sends or steers the readable direct prompt. The target log contains two simple messages, so later source mutation cannot change replay and transcript consumers do not need a prompt envelope. This supersedes the attachment mechanism in the [cross-session reference decision](../../implemented/feature/2026-07-21-cross-session-references.md) while retaining its snapshot and trust-boundary rules.
|
||||
|
||||
This proposal preserves the caller-owned framing decision from [unwrapped injected content](../../implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md), the one-item turn rule from [one send, one turn](../../implemented/simplification/2026-07-17-one-send-one-turn.md), and narrows the [turn-enclosure decision](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md) so turns enclose execution rather than every session event.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep `SendOptions.contexts` as an atomic attachment.** This preserves all-or-nothing delivery when prompt admission blocks, but it keeps context inside inbox lifecycle state and requires every queue transition and observation event to carry it. The generic agent API should not encode a domain transaction that most callers can express as context injection followed by message delivery.
|
||||
|
||||
**Keep a distinct `context/message` session event.** A separate event makes the out-of-turn exception narrower, but user-role model input would again have two event types with identical projection. `user/message.source` already carries the distinction needed by policy, transcript, and replay consumers.
|
||||
|
||||
**Keep one-shot turns for idle injection.** This retains universal turn enclosure and a convenient flush boundary, but it makes turn counts and turn observers report work that never ran the model. Durability is an independent session concern and can be awaited without fabricating execution.
|
||||
|
||||
**Keep `prompt-prefix` as an optional placement.** Prefix baking can make the context and request appear in one provider message, but it introduces a second representation of the direct prompt and spreads placement handling across admission, steering, logging, replay, and UI code. Producers that require textual framing may include it in their own context content.
|
||||
|
||||
**Let hooks call `inject()` directly instead of returning additional contexts.** Direct injection would erase the extension point's admission ownership: a listener could append context before a downstream listener blocks the operation. Returning `additionalContexts` keeps the waterfall result authoritative while sharing the same post-admission outbox path.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `SendOptions` and steering inbox records contain no attached contexts; `agent/queued` reports only the retained message and steering facts.
|
||||
- `AdditionalContext` replaces `HookContext` across prompt interception, tool execution, hook bridges, guards, and context producers, with only `content` and `source`.
|
||||
- Prompt-prefix placement, prompt envelopes, and `context/message` are absent from public types, durable events, projection, and UI replay.
|
||||
- Idle `inject()` appends and flushes one sourced `user/message` without a turn or model call; `whenIdle()` and disposal await the flush.
|
||||
- Active-turn injection and hook-produced contexts drain at safe boundaries after complete tool-result batches and before the request that consumes them.
|
||||
- Blocked prompt admission opens no turn and appends neither the prompt nor hook-produced additional contexts; independently injected caller context remains.
|
||||
- Unit, persistence/resume, invariant, ACP/TUI replay, and keyless assembled-application snapshots cover the new event order and durability semantics.
|
||||
|
||||
## Risks
|
||||
|
||||
- Allowing one surface event outside turns weakens a simple invariant and may expose hidden assumptions in persistence scanning, crash repair, forking, compaction, and session queries.
|
||||
- Consecutive user-role messages replace one baked prompt message; provider adapters and cache behavior must accept and preserve that ordering.
|
||||
- `inject()` followed by a blocked `send()` leaves context without its intended direct prompt unless the caller accepts the independent-commit contract.
|
||||
- A synchronous injection API cannot return flush failure. Logging alone is less structured than `agent/error`, while adding a new persistence event solely for this case may create another unnecessary seam.
|
||||
- Removing attachment, placement, metadata, envelopes, and a durable event type is a broad pre-release migration that must update every producer and consumer atomically.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Agent Note: 将上下文注入与轮次执行分离
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-24-separate-context-injection-from-turn-execution.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
agent API 目前用三种相互重叠的方式表示面向模型的补充输入:调用方通过 `SendOptions.contexts` 附加 `HookContext[]`,拦截钩子和工具钩子返回 `additionalContexts`,插件则调用 `agent.inject()`。这些路径最终都会把上下文写入同一份模型历史,但各自携带不同的放置、元数据、准入、队列和轮次生命周期规则。
|
||||
|
||||
将上下文原子附加到收件箱消息后,agent loop(智能体循环)必须让上下文跟随提示词准入、steering(中途引导)转换、取消和终止丢弃的完整生命周期。`prompt-prefix` 放置方式又会把上下文与直接提示词合并为一个事件,因此 transcript(文本记录)消费方需要依赖模型不可见的封套,才能还原用户实际输入。这样一来,outbox 条目、会话投影和 UI 回放都必须处理本应由生产方负责的区分。
|
||||
|
||||
空闲状态下的 `inject()` 还暴露了另一处语义错位。注入并不请求模型执行,但当前实现仅为了满足轮次封闭不变量并获得持久性检查点,就会打开并关闭一个零步骤的 `injection` 轮次。于是,轮次有时表示「运行 agent loop」,有时却表示「不运行 agent,仅持久化上下文」。
|
||||
|
||||
`HookContext` 的名字也描述了生产方,而非该值的职责。它可能来自原生插件、hook bridge、提示词准入或工具后处理;其稳定含义只是带来源信息的额外模型上下文。
|
||||
|
||||
## 提案
|
||||
|
||||
将 `inject()` 设为调用方添加补充模型输入的唯一操作,并把轮次严格定义为一次模型循环执行。
|
||||
|
||||
移除 `SendOptions.contexts`。拥有上下文的调用方通过 `inject()` 交付上下文,再独立使用 `send()` 或 `steer()` 提交直接消息。将 `HookContext` 重命名为 `AdditionalContext`;这个共享结构只保留 `content` 和 `source`,移除放置方式与模型不可见元数据。
|
||||
|
||||
提示词和工具扩展点仍可返回 `additionalContexts`。这些值是扩展点的输出,而不是从调用方收件箱条目捕获的附件。提示词准入在 `run()` 打开轮次之前执行。提示词获准后,它与返回的额外上下文一同进入 outbox;提示词被阻止时,两者都不写入,也不打开轮次。工具产生的额外上下文则在对应工具结果之后进入同一个 outbox。
|
||||
|
||||
每项额外上下文都成为独立的 `user/message`,并由 `source` 记录来源。移除 `context/message`、prompt-prefix 放置方式、稳定请求分隔符和提示词封套。transcript 与 UI 消费方通过 `source` 区分直接用户消息和注入上下文,无需从合并后的模型内容中恢复隐藏的直接提示词字段。
|
||||
|
||||
## 注入生命周期
|
||||
|
||||
轮次打开时,`inject()` 将上下文暂存在 loop outbox 中。agent loop 会在安全的步骤边界排空 outbox,同时保持工具协议要求的相邻关系:在助手工具调用批次期间接纳的上下文,只能出现在该批次所有有序结果之后。系统整体取走 outbox,确保同一边界接纳的 steering 和注入上下文对后续同一次请求可见。
|
||||
|
||||
没有打开的轮次时,`inject()` 会立即追加对应的 `user/message` 并启动会话刷新。它不会增加轮次编号、发出 `turn/start` 或 `turn/end`、改变 agent 状态,也不会运行模型。同步 API 仍会在异步刷新完成前返回;`whenIdle()` 和 agent dispose(资源释放)会把尚未结束的空闲注入刷新纳入静止边界。
|
||||
|
||||
空闲刷新失败时不存在合法的轮次或步骤坐标。系统通过日志或持久化所属的错误接口报告该失败,而不是为不存在的轮次伪造 `agent/error` 载荷。内存中的事件仍已接纳,后续刷新可以重试持久化。
|
||||
|
||||
因此,会话不变量允许 `user/message` 位于两个轮次之间,同时继续要求执行事件、steering、助手输出、工具事件以及默认的包扩展事件均受轮次边界约束。持久化、恢复、resume、fork、压缩和查询逻辑必须把合法的轮次外 `user/message` 当作已提交会话历史,而不是中断轮次或可丢弃的日志尾部。
|
||||
|
||||
## 扩展点与调用方语义
|
||||
|
||||
`PromptDecision.content` 仍只替换直接提示词。`PromptDecision.additionalContexts` 和工具结果的 `additionalContexts` 保留 FIFO 顺序及各自来源,但不再选择放置方式。waterfall(瀑布式事件)监听器调用 `next()` 委托时,必须保留下游返回的提示词内容和额外上下文,除非它有意返回替代值。
|
||||
|
||||
调用方主动注入与钩子产生的额外上下文具有不同的准入归属。钩子的额外上下文只会在该钩子允许提示词或工具结果后落入日志。调用方执行 `inject(context)` 后再执行 `send(prompt)` 时,上下文已独立提交;后续提示词准入即使阻止该提示词,注入上下文仍保留在历史中。需要领域级全有或全无语义的调用方,必须在执行任一操作前自行完成准备,或提供领域专用的准入 seam。
|
||||
|
||||
跨会话引用使用普通组合方式:宿主先准备快照,以会话引用来源调用 `inject()`,再发送或 steer 可读的直接提示词。目标日志包含两条简单消息,因此来源会话后续变化不会改变回放,transcript 消费方也不需要提示词封套。本提案取代[跨会话引用决策](../../implemented/feature/2026-07-21-cross-session-references.md)中的附件机制,但保留其快照与信任边界规则。
|
||||
|
||||
本提案保留[移除注入内容封套](../../implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md)确立的调用方自主管理框架原则,以及[一次 send、一个轮次](../../implemented/simplification/2026-07-17-one-send-one-turn.md)确立的单条目轮次规则;同时收窄[轮次封闭决策](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md),使轮次约束执行过程,而不是约束所有会话事件。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留 `SendOptions.contexts` 作为原子附件。** 提示词准入阻止消息时,这种方式能保留全有或全无交付,但也会让上下文继续成为收件箱生命周期状态的一部分,并迫使每次队列转换和观察事件携带它。大多数调用方都可以通过先注入上下文、再交付消息来表达需求,通用 agent API 不应内置领域事务。
|
||||
|
||||
**保留独立的 `context/message` 会话事件。** 独立事件可以缩小轮次外事件的例外范围,但面向模型的 user-role 输入会再次拥有两个投影完全相同的事件类型。`user/message.source` 已能为策略、transcript 和回放消费方提供所需区分。
|
||||
|
||||
**为空闲注入保留一次性轮次。** 这种方式能保留通用轮次封闭和方便的刷新边界,却会让轮次计数与轮次观察方报告从未运行模型的工作。持久性是独立的会话关注点,无需伪造执行即可等待。
|
||||
|
||||
**保留 `prompt-prefix` 可选放置方式。** 前缀烘焙可以让上下文和请求位于同一条提供方消息中,但它会引入直接提示词的第二种表示,并把放置处理扩散到准入、steering、日志、回放和 UI 代码。需要文本框架的生产方可以直接把它写入自身上下文内容。
|
||||
|
||||
**让钩子直接调用 `inject()`,而不是返回额外上下文。** 直接注入会破坏扩展点的准入归属:下游监听器阻止操作之前,上游监听器就可能已经追加上下文。返回 `additionalContexts` 能维持 waterfall 结果的最终权威性,同时复用准入后的 outbox 路径。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- `SendOptions` 与 steering 收件箱记录不再包含附加上下文;`agent/queued` 只报告保留的消息和 steering 事实。
|
||||
- `AdditionalContext` 在提示词拦截、工具执行、hook bridge、guard 和上下文生产方中取代 `HookContext`,且只包含 `content` 与 `source`。
|
||||
- 公共类型、持久事件、投影和 UI 回放中均不存在 prompt-prefix 放置方式、提示词封套与 `context/message`。
|
||||
- 空闲 `inject()` 在不产生轮次或模型调用的情况下,追加并刷新一条带来源的 `user/message`;`whenIdle()` 和 dispose 会等待该刷新。
|
||||
- 活跃轮次注入和钩子产生的上下文会在完整工具结果批次之后的安全边界排空,并在消费它们的请求之前进入日志。
|
||||
- 被提示词准入阻止的消息不会打开轮次,也不会追加提示词或钩子产生的额外上下文;调用方此前独立注入的上下文仍保留。
|
||||
- 单元测试、持久化与 resume 测试、不变量测试、ACP/TUI 回放测试,以及无需密钥的组装应用快照覆盖新的事件顺序和持久性语义。
|
||||
|
||||
## 风险
|
||||
|
||||
- 允许一个表层事件位于轮次之外,会削弱一条简单不变量,并可能暴露持久化扫描、崩溃恢复、fork、压缩和会话查询中的隐含假设。
|
||||
- 两条连续的 user-role 消息会取代一条烘焙后的提示词消息;提供方适配器和缓存行为必须接受并保留这一顺序。
|
||||
- 如果调用方不能接受独立提交契约,`inject()` 后跟一个被阻止的 `send()` 会留下缺少预期直接提示词的上下文。
|
||||
- 同步注入 API 无法返回刷新失败。只记录日志的结构化程度低于 `agent/error`,但仅为此场景增加新的持久化事件也可能产生另一个不必要的 seam。
|
||||
- 移除附件、放置方式、元数据、封套和一种持久事件类型,是一次影响面较广的预发布迁移,必须原子更新所有生产方和消费方。
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-06-30-pre-tool-input-rewrite.md: 5de605e7edf63046b14a125d53072aab9bb5d37e
|
||||
2026-06-30-pre-tool-input-rewrite.zh.md: eab80764f5dfbd0448dabf0dc3c893ba2ec94928
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-06-30-pre-tool-input-rewrite.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [interception-seams Agent Note](../../implemented/feature/2026-06-30-interception-seams.md) defines `tools/pre-execute` as an allow/deny/ask gate over an execution whose identity is already protected and whose arguments are deeply frozen. Claude Code's `PreToolUse` hook also offers `updatedInput`, so a faithful bridge needs an explicit rewrite mechanism. A rewrite cannot be a mutation escape hatch on the existing execution object: it must keep the durable history, audit record, presentation, and executed value consistent.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Agent Note: 工具执行前输入重写——一致性设计
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-06-30-pre-tool-input-rewrite.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[拦截 seam Agent Note(agent 决策记录)](../../implemented/feature/2026-06-30-interception-seams.md) 将 `tools/pre-execute` 定义为一道针对执行的允许/拒绝/询问门禁,此时执行的身份标识已受保护、参数已被深度冻结。Claude Code 的 `PreToolUse` 钩子还提供了 `updatedInput`,因此忠实的桥接需要一个显式的重写机制。重写不能是对现有执行对象的可变逃逸口:它必须保持持久化历史、审计记录、展示层与实际执行值之间的一致性。
|
||||
|
||||
## 问题本质:执行前参数的三个读取方
|
||||
|
||||
在 agent loop(智能体循环)中,工具调用的参数在工具执行之前就已提交到日志并被实时消费方读取:
|
||||
|
||||
1. **`assistant/message`** 在工具分发之前追加——它是 `deriveMessages()` 回放时的模型历史来源,因此携带模型自身输出的工具调用参数。
|
||||
2. **`tool/call`** 是持久化的审计记录,在 `ctx.tools.execute()` 之前追加。
|
||||
3. **展示层实时读取 `tool/call.arguments`**:ACP(Agent Client Protocol)桥接记住这些参数并传给 `presentResult`;`dsh-tool-bash` 从中派生卡片标题、rawInput、cwd 以及终端/后台处理方式。
|
||||
|
||||
如果只做执行层面的重写,UI 会显示一条命令而实际运行的是另一条,并且结果会对着错误的参数渲染。注册表目前通过以下方式防止这种失败模式:对 `arguments` 做 structured-clone 并深度冻结,将执行身份属性设为不可写,且不暴露任何可替换它们的测试 shim 或监听路径。重写设计必须维护这一受保护的身份边界,而非削弱它。
|
||||
|
||||
## 提案
|
||||
|
||||
重写是一个「身份标识创建前的一致性事务」。当钩子提供 `updatedInput` 时,有效值必须在注册表构造其不可变的 `ToolExecution` 之前确定,并且必须原子地反映到全部三个读取方:
|
||||
|
||||
- `tool/call` 审计事件记录重写后的参数(原始参数保留在一个伴随字段中,作为审计线索——钩子修改了调用,原始参数与生效参数都是值得保留的事实)。
|
||||
- 派生历史中的 `assistant/message` 必须与实际执行一致。待评估的选项:就地重写 assistant 消息中的工具调用块(改变模型「看到自己说了什么」),或记录一条单独的修正让下一次请求携带。Claude Code 的模型是让模型看到重写已生效。
|
||||
- 展示层(`presentCall`/`presentResult`)读取重写后的参数,使 UI 显示实际运行的内容。
|
||||
|
||||
在 `PreToolDecision` 当前的触发点上做扩展是不够的:此时两条持久化记录已经存在,执行身份已受保护。实现必须将相关决策移到日志提交之前,或者增加一个专门的、更早的重写决策点来处理待定的模型调用。agent loop 将生效参数提交到历史和审计之后,再构造普通的不可变执行对象,并照常运行现有的允许/拒绝/询问和工具流水线。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不直接修改执行对象?
|
||||
|
||||
允许 pre-execute 监听器赋值 `exec.arguments` 只能提供执行层面的重写,模型历史、审计和展示层不会随之改变。保持身份标识受保护使得这种局部行为不可表达。在一致性事务实现之前,CC/Codex 桥接对 `updatedInput` 记录日志并发出警告,而非声称已兑现;循环分发点的 `TODO(pre-tool-input-rewrite)` 标记了缺失的更早阶段。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 请求的重写在 `ToolExecution` 身份标识创建之前解决,并原子地反映到全部三个读取方:`tool/call` 审计记录重写后的参数(原始参数保留在伴随字段中)、派生历史与实际执行一致、展示层渲染重写后的参数。
|
||||
- 生效的 `ToolExecution.arguments` 在 pre-policy、守卫、分发、post-policy 和最终观测全程保持深度冻结且不可写;不引入任何可变 shim。
|
||||
- CC/Codex 桥接兑现 `updatedInput`,不再记录忠实但降级的警告。
|
||||
|
||||
## 风险
|
||||
|
||||
- 重写 `assistant/message` 中的工具调用块会改变模型「看到自己说了什么」;是否有提供方在回放时拒绝这种改动,是一个需要通过实验确定的开放问题,必须在决策形状冻结之前解决。
|
||||
- 更早的重写阶段改变了 `assistant/message`、`tool/call`、钩子审计事件与执行之间的顺序关系;设计必须固定这一顺序,同时不削弱轮次封闭性或调用/结果邻接性。
|
||||
|
||||
## 开放问题
|
||||
|
||||
- 重写 `assistant/message` 中的工具调用块是否会破坏某些提供方在回放时的预期?还是单独的修正更安全?
|
||||
- 原始参数是否应保留在 `tool/call` 事件(审计)上?如果是,放在什么字段?
|
||||
- 重写决策是移到日志提交之前,还是成为一个专门的更早 seam?现有的 pre-tool 允许/拒绝钩子如何避免运行两次?
|
||||
- 这与未来的权限 `ask` 流程(用户批准一个被重写的调用)如何交互?
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-07-claude-code-and-codex-subagent-backends.md: ee8576f97a9fdef8c88dcad3a73f28b63ca3ebe1
|
||||
2026-07-07-claude-code-and-codex-subagent-backends.zh.md: 14e8dde04d9526aaffc0e58be049e13858362887
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-07-07-claude-code-and-codex-subagent-backends.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The subagent seam ([the seam Agent Note](../../implemented/feature/2026-06-21-subagent-capability-seam.md)) hosts multiple named providers on `ctx.subagents`, and the ACP backend ([the ACP backend Agent Note](../../implemented/feature/2026-06-22-acp-subagent-backend.md)) proved the seam generalizes across a process boundary; its Future-providers section explicitly named the Codex app-server and the Claude Code Agent SDK as mechanically similar siblings. Those two are the engines actually worth delegating to today: a harness turn should be able to hand a self-contained task to a real Claude Code or a real Codex — a separate product with its own model, tools, and sandbox — and get back one final answer, without the parent deployment leaking its secrets into the child or the child's behavior silently depending on whatever `~/.claude` / `~/.codex` state exists on the host machine.
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Agent Note: Claude Code 与 Codex subagent 后端(向外部编码 agent(智能体)的进程外委派)
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-07-claude-code-and-codex-subagent-backends.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
subagent seam([seam Agent Note(agent 决策记录)](../../implemented/feature/2026-06-21-subagent-capability-seam.md))在 `ctx.subagents` 上托管多个命名提供方,ACP(Agent Client Protocol)后端([ACP 后端 Agent Note](../../implemented/feature/2026-06-22-acp-subagent-backend.md))证明了该 seam 能跨越进程边界泛化;其「未来提供方」一节明确将 Codex app-server 与 Claude Code Agent SDK 列为机械上相似的兄弟。如今真正值得委派的就是这两个引擎:harness 的一个轮次应能把一个自包含任务交给真实的 Claude Code 或真实的 Codex——一个拥有自身模型、工具与沙箱的独立产品——并取回一个最终答案,同时父部署不向子进程泄漏密钥,子进程行为也不静默依赖宿主机上碰巧存在的 `~/.claude` / `~/.codex` 状态。
|
||||
|
||||
## 提案
|
||||
|
||||
两个兄弟提供方包(package),作为 ACP 后端的结构变体,另加一次提取:
|
||||
|
||||
- `@deepseek-ai/dsh-subagent-claude-code`:通过 `@anthropic-ai/claude-agent-sdk` 的 `query()` 驱动一个 Claude Code 子进程(SDK 在父进程中运行,并将其内置的 `claude` CLI(命令行界面)作为子进程 spawn)。提供方名称为 `claude-code`:子进程是 Claude Code 这个*产品*,而非 Anthropic 模型适配器——「claude」保留给未来的 `dsh-llm` 适配器。
|
||||
- `@deepseek-ai/dsh-subagent-codex`:spawn `codex app-server`,通过其 JSON-RPC-over-stdio 协议驱动一个 thread/turn,使用包内一个手写的换行 JSON 客户端(约 200–300 行)。
|
||||
- `@deepseek-ai/dsh-subagent-process`:纯库(沿用 `subagent-inprocess` 的先例),提取 `dsh-subagent-acp` 已有且两个新后端都需要的内容:凭证环境清洗(`buildChildEnv`)、EOF → SIGTERM → SIGKILL 的 dispose(资源释放)阶梯,以及新的隔离配置目录辅助函数(`mkdtemp` 创建、尽力删除)。ACP 后端迁移到该库上;`bash-local` 的兄弟副本保持不动以限制变更范围。
|
||||
|
||||
两个提供方逐字复制 ACP 后端的 seam 姿态:每次 `start` 创建全新子进程、恰好一次提示词往返、所有能力均为 `false`、`inheritsParentContext: false`、忽略 `request.parent`/`request.agentOptions`、`id = SessionId(randomUUID())`,且 `result` 从不 reject——子进程级失败扁平化为 stop reason,原始错误则通过 `onError` spec 回调送到 `ctx.logger`。模型暴露无需新代码:每个提供方各加载一次 `dsh-tool-subagent`,使用不同的 `toolName`(`subagent_claude_code`、`subagent_codex`)。无需新的会话事件——唯一的模型可见产物是工具结果,因此可重建性与 ACP 完全相同。明确边界:会话日志重建模型可见的 transcript(文本记录),而不是工作区变更历史——获准写入的子进程将文件作为日志之外的环境副作用进行修改,与 bash 工具和 ACP 后端现有行为完全一致;回放复现请求,而非磁盘。
|
||||
|
||||
## 已验证的接口事实(固定版本)
|
||||
|
||||
两个集成面在本提案之前均已针对固定版本进行了验证——阅读类型与打包源码、运行无需密钥的 spike——而非仅依赖厂商文档。固定版本是验证基线,不是运行时契约:后端不执行运行时版本探测(无 `codex --version` 门禁、无 SDK 版本嗅探)。兼容性在开发时强制执行——每次依赖升级都会针对真实加载路径重跑无密钥套件——在运行时则通过大声失败来保障:协议层面的意外通过 `onError` 结算为 `error`,绝不静默异常。
|
||||
|
||||
**`@anthropic-ai/claude-agent-sdk` 0.3.202。** `options.env` 会替换子进程环境(不与 `process.env` 合并),恰好满足清洗需求。`settingSources` 默认加载所有文件系统设置——隔离要求显式传入 `[]`。结果子类型为 `success` | `error_during_execution` | `error_max_turns` | `error_max_budget_usd` | `error_max_structured_output_retries`。中止时 SDK 自行逐级加强对 CLI 子进程的终止措施:立即关闭 stdin,约 2 秒后若子进程未退出则发送 SIGTERM(已观察到;无残留进程)——无需自定义 kill 回退。`outputFormat: {type: 'json_schema'}` 和 `agents` 选项已存在,为 seam 的 `outputSchema` 能力和命名 subagent 类型提供了未来着陆点;两者均不在本 Agent Note 范围内。
|
||||
|
||||
**codex CLI 0.142.5,`codex app-server`(v2 词汇)。** LF 分隔的 JSON,JSON-RPC 2.0 形状但省略 `"jsonrpc"` 头。
|
||||
|
||||
- 生命周期:`initialize{clientInfo}` + `initialized` → `thread/start`(接受 `cwd`、`model`、`sandbox`、`approvalPolicy`、`ephemeral`;未认证即可成功)→ `turn/start{threadId, input:[{type:'text',text}]}` 立即返回一个 `inProgress` 的轮次;终止信号是携带 `Turn{status: completed|interrupted|failed|inProgress, error}` 的 `turn/completed` 通知。
|
||||
- 审批是服务端发起的请求——`item/commandExecution/requestApproval`、`item/fileChange/requestApproval`、`item/permissions/requestApproval`、`item/tool/requestUserInput`、`mcpServer/elicitation/request`——以 `accept`/`decline` 系列决策应答。
|
||||
- 认证:`account/login/start{type:'apiKey', apiKey}` 是一等 RPC,`account/read` 报告 `requiresOpenaiAuth`——且未认证的 `turn/start` 不会快速失败(它会挂在重试中),因此后端必须预检认证状态,并在失败时大声结算为 `error`,而非等待轮次。
|
||||
- 隔离:`CODEX_HOME` 重定向被尊重(`initialize` 响应会回显它,测试可据此断言隔离),`ephemeral: true` 的 thread 不留任何会话文件。
|
||||
|
||||
## 隔离与凭证
|
||||
|
||||
部署只使用 API key 认证,子进程不得看到宿主用户的 Claude Code / Codex 配置:行为必须只由 `cordis.yml` 决定。每次运行获得一个全新的 `mkdtemp` 配置目录——Claude Code 使用 `CLAUDE_CONFIG_DIR`(并显式设置 `settingSources: []`),Codex 使用 `CODEX_HOME`——dispose 时尽力删除;配置字段也可以固定一个持久目录。子进程环境通过提取逐字复用 ACP 后端的 `buildChildEnv` 语义:转发环境变量,但移除凭证形态的变量(`/KEY|SECRET|TOKEN/i`),再叠加 `config.env`——因此 `PATH`、`HOME`、`TMPDIR`、locale 和代理变量保留,CLI 正常运行;只有环境中的凭证形态变量被清洗(Claude Code 的 `ANTHROPIC_API_KEY` 通过 `config.env` 显式进入),Codex key 则通过 `account/login/start` RPC 进入隔离的 `CODEX_HOME`,而非手写 `auth.json`。
|
||||
|
||||
## 权限与审批策略
|
||||
|
||||
每个后端不压缩为 ACP 单一的 `permission: allow|reject` 旋钮,而把引擎原生词汇作为配置暴露,并采用保守默认值:Claude Code 获得 `permissionMode`(默认 `default`)以及 `permission: allow|reject`(默认 `reject`),后者作为所有漏过请求的 `canUseTool` 自动应答;Codex 获得 `sandboxMode`(默认 `read-only`)和 `approvalPolicy`(默认 `never`),以及同一个 `permission` 后备值,用来应答仍然到达的审批请求。默认值刻意做到不造成损害(开箱即用的子进程无法写文件);示例演示如何开放权限(`acceptEdits` / `workspace-write`)。机械规则是:每一个服务端发起的请求都由程序迅速结算——枚举出的审批/用户输入/elicitation 请求按配置策略应答,未知请求方法用 JSON-RPC method-not-found 错误响应(绝不保持 pending),未知通知被消费——因此任何子进程请求都不会因等待永远不会到来的应答而卡住轮次。这一版中提示词不会到达人类,与 ACP 一致。
|
||||
|
||||
## StopReason 映射
|
||||
|
||||
Claude Code:`success` → `completed`;`error_max_turns`、`error_during_execution`、`error_max_budget_usd`、`error_max_structured_output_retries` → `error`(与 ACP 对 `max_turn_requests` 的处理对齐:未完成的任务不是成功);生成器中止 → `aborted`;未知值 → `error`。Codex:`Turn.status` 为 `completed` → `completed`;`interrupted` → `aborted`;`failed` 且 `codexErrorInfo: 'contextWindowExceeded'` → `max-tokens`,其他 `failed` → `error`;传输/spawn/认证预检失败 → `error`(若已请求取消则为 `aborted`)。两者中,`cancel()` 采用 ACP 形状:标志位 + abort/interrupt + 一个 cancel-settled 竞争分支,使不合作的子进程无法阻塞结果。
|
||||
|
||||
活性姿态,明确声明:teardown 时序是配置项,轮次时长不是。两个后端将 dispose 阶梯的宽限期作为带默认值的已验证配置字段(ACP 后端的 `disposeEofGraceMs`/`disposeGraceMs` 形状,由提取库承载),但刻意不设轮次时长或启动超时——与 ACP 一致:轮次期间的活性由调用方通过 `cancel()`/abort signal 掌控,subagent 轮次持续数分钟也属合理,而 Codex 认证预检消除了唯一已验证的必然挂起场景;需要墙钟上限的部署从父侧取消即可。
|
||||
|
||||
## 测试
|
||||
|
||||
依照根 AGENTS.md 规则在每个层级明确命名,并预先消除风险:
|
||||
|
||||
- **无密钥单元/集成测试**:每个后端都镜像 ACP spec 清单(往返和输出累积、每种 stop 映射、两条取消路径、已中止、两种策略下的权限自动应答、未知消息容错、错误命令的 spawn 失败、HMR(热模块替换)提供方清理、导出形状、子进程环境隔离断言和临时目录删除;Codex 另加认证预检失败路径)。Claude Code harness 是通过 `pathToClaudeCodeExecutable` 接入真实 SDK 的脚本化假 `claude` 可执行文件——一个 spike 已在 24ms 内完成端到端无密钥验证(假 CLI 应答一次 `control_request/initialize`,并讲 plain stream-json,约 40 行)。Codex harness 是讲已验证协议格式的脚本化 mock app-server 子进程,沿用 `mock-acp-server.ts` 形状。
|
||||
- **有密钥 e2e 测试**:每个后端的真实引擎执行并由磁盘验证真实文件工作,固定使用开放后的配置,以免验收与不造成损害的默认值冲突——Claude Code 使用 `permissionMode: 'acceptEdits'`,Codex 使用 `sandboxMode: 'workspace-write'` + `approvalPolicy: 'never'`;自跳过会准确报告缺失的是二进制还是 key。CI 没有密钥,因此依照有密钥策略在本地运行。
|
||||
- **快照测试**:以 `TODO(claude-code-subagent-replay)` / `TODO(codex-subagent-replay)` 推迟——即 ACP 后端也推迟的独立回放形状([按会话回放 Agent Note](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md));在此期间由无密钥套件提供确定性覆盖。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不用官方 `@openai/codex-sdk` 而手写客户端?
|
||||
|
||||
dispose 阶梯和环境清洗要求拥有子进程(spawn 参数、env、信号、exit 等待);SDK 隐藏了进程。协议格式(wire format)极其简单(LF JSON),形状可按固定版本生成(`codex app-server generate-json-schema`),仓库先例(`hook-protocol`)是拥有薄协议核心而非包装他人的运行时。SDK 能节省协议演进的维护成本,但代价是失去本后端存在的意义所在的精确控制。
|
||||
|
||||
### 为什么不用模型可见的 `subagent_type` 参数(单一 Task 风格工具)?
|
||||
|
||||
Claude Code 自身的 Task 工具将 subagent 类型放在模型可见的 schema 中,选择一个提示词 + 工具集人格。这里的选择是在执行引擎之间做出的,而只有部署者知道哪些引擎配置了凭证——因此选择留在部署配置层,保持 `dsh-tool-subagent` 文档中的「一个提供方对应一个工具」契约。人格风格的类型选择器应是针对工具的另一个 Agent Note,而非针对后端。
|
||||
|
||||
### 为什么不用登录态凭证和用户自身的配置?
|
||||
|
||||
继承 `~/.claude` / `~/.codex`(订阅登录、用户设置、skill(技能)、MCP 服务器)会使子进程行为依赖宿主机状态,并在 ACP 后端和 bash 执行器确立的「凭证通过 `config.env` 显式进入,绝不隐式继承」规则上打开一个隐式例外。仅 API key 加强制配置目录隔离使运行可复现;需要共享状态的部署可以有意将配置目录字段指向一个持久目录。
|
||||
|
||||
### 为什么不为 Claude Code 无密钥测试注入驱动层 seam?
|
||||
|
||||
注入假的 `query()` 会 mock 我们自己的边界,使真实 SDK 加载路径未被测试(docs/testing.md 中的 real-over-mock 策略)。曾考虑此方案的风险——SDK↔CLI 的 stream-json 控制协议是内部实现——已被 spike 消除:假 CLI harness 今天能对真实固定版本的 SDK 正常工作。如果 SDK 升级破坏了 mock,无密钥套件会让升级 PR(Pull Request)失败,这正是门禁在发挥作用。
|
||||
|
||||
### 为什么不用 ACP 适配器(如 `claude-code-acp`)复用既有后端?
|
||||
|
||||
社区 shim 将两个引擎包装为 ACP,这会使它们在 `dsh-subagent-acp` 上变成「仅配置」。但这在 harness 与引擎之间插入了一个非官方的第三方层,抹去了本 Agent Note 暴露的原生控制面(permissionMode、sandboxMode/approvalPolicy、配置目录隔离、apiKey RPC),并以 shim 的发布节奏替换了第一方协议的稳定性。第一方接口——Agent SDK 和 app-server——才是受支持的集成点。
|
||||
|
||||
## 验收标准
|
||||
|
||||
在两个引擎和密钥均已配置的机器上:一个 REPL 驱动的模型通过 `subagent_claude_code` 完成一个真实文件任务,通过 `subagent_codex` 完成另一个,工具结果为子进程的最终答案,父会话日志中仅有 `tool/call` + `tool/result`。无密钥套件在无凭证环境下以逐文件 100% 覆盖率通过,断言隔离(清洗后的子进程环境、dispose 后无残留临时配置目录),并断言 `~/.claude` / `~/.codex` 的存在与否不影响子进程行为。取消父轮次后,两个后端在有界时间内完全停稳,无残留子进程。e2e 套件干净地自跳过,命名缺失的前置条件。
|
||||
|
||||
## 风险
|
||||
|
||||
- `codex app-server` 被 CLI 标记为实验性,其 v1/v2 词汇共存;客户端固定 0.142.5、仅实现 v2、对未知方法/通知消费而不崩溃,但未来 codex 升级仍可能迫使返工(每次升级重新生成 schema 并重跑无密钥套件——这是上述「不做运行时版本探测」立场背后的开发时强制执行)。
|
||||
- Claude Code 假 CLI mock 依赖一个内部协议:任何 SDK 升级都必须通过无密钥套件,控制协议的破坏性变更意味着返工 mock(回退方案:上面否决的驱动注入 seam 成为逃生舱口)。
|
||||
- SDK 的 optionalDependencies 每平台约 280MB——已接受,限制在单个后端包内。
|
||||
- SDK 的 SIGKILL 分支(EOF→SIGTERM 之后)未被观察到,信任其实现;e2e 保留无残留进程断言。
|
||||
- Codex 是部署前置条件(无 npm 内置二进制);缺失或不兼容的二进制以大声的 spawn/协议 `error` 呈现,而非版本探测。
|
||||
- 每次运行付出一个全新子进程的代价,且仅最终答案浮出——思考、工具卡片和用量被消费后丢弃;连接池、中间进度浮出、`sendMessage`/`resume`、通过 SDK 的 `outputFormat` 实现 `outputSchema`、以及通过 SDK 的 `agents` 选项实现命名 subagent 类型,均为刻意推迟。
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-08-interactive-side-sessions.md: dfd325babe215782c9c1cbec3fd9f874783af7ab
|
||||
2026-07-08-interactive-side-sessions.zh.md: 9bc9d5c94fdef893134844551a594acc39a99d3b
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-07-08-interactive-side-sessions.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A user may want to explore a question from a live session without changing its main context. Existing primitives do not expose that product shape: [session-store fork](../../implemented/feature/2026-06-30-session-store-fork-api.md) creates an unattached session, while [fork subagents](../../implemented/feature/2026-06-21-subagent-capability-seam.md) are model-driven tasks whose transcript collapses into one tool result. Neither gives the user a separate conversation, and neither records a conclusion back into the parent with provenance.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: 交互式侧会话与合并回写
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-08-interactive-side-sessions.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
用户可能希望在不改变当前会话主上下文的前提下,探索一个来自活跃会话的问题。现有原语无法提供这种产品形态:[会话存储 fork](../../implemented/feature/2026-06-30-session-store-fork-api.md)创建的是一个无关联的会话,而 [fork subagent](../../implemented/feature/2026-06-21-subagent-capability-seam.md)是模型驱动的任务,其 transcript(文本记录)会折叠为一条工具结果。两者都不能给用户一个独立的对话,也都不能将结论带着出处信息记录回父会话。
|
||||
|
||||
## 提案
|
||||
|
||||
**侧会话(side session)** 是一个普通的活跃会话,从源会话的最后一个已完成轮次 fork 而来,绑定到自己的 agent(智能体),定位为只读顾问,并能**合并回写**一条精简笔记。
|
||||
|
||||
- **Fork 并绑定:** 以父会话的平衡已完成轮次前缀创建子会话,并在其元数据中标记 `parentSession` 与 `seedLength`。这组合了 `ctx.agents.create({ seed, meta })`;不新增核心服务或会话存储方法。
|
||||
- **顾问定位:** 创建后注入一条插件来源的 `context/message`,告知子会话只做解释,不执行变更或继续任务。保持系统提示词逐字节一致,可在继承的历史上保留提供方的前缀缓存。
|
||||
- **合并回写:** 向子会话请求一条有长度上限的 handback,然后向父会话注入一条插件来源的 `context/message`。父会话的下一次请求在其日志位置看到该消息,保持回放与[请求可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md),无需新增会话事件。
|
||||
- **呈现:** 调用方式、会话切换与 handback 渲染属于首个客户端拥有的界面。本 Agent Note(agent 决策记录)仅规定与界面无关的机制。
|
||||
|
||||
回退产品化、会话树视图、面向模型的侧会话工具,以及 `forkName`/`mergedInto` 元数据均不在本 Agent Note 范围内。一次真实适配器 spike 已验证了源日志隔离、继承上下文、多轮子会话交互,以及合并回写在父会话下一轮次中的可见性。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **使用 subagent seam:** 否决。侧会话是用户驱动的、客户端可见的,且可能存活超过父会话的一个轮次;subagent 是模型驱动的运行,返回一条工具结果。
|
||||
- **修改子会话的系统提示词:** 默认否决,因为任何字节变化都会从第零个 token 起使前缀缓存失效。部署方仍可选择这种更强的隔离方式。
|
||||
- **新增 `sidechat/*` 事件:** 延后。插件来源的 `context/message` 已提供持久性、出处与回放能力;只有当某个界面需要差异化渲染时,专用事件才有正当理由。
|
||||
- **现在就绑定一个协议界面:** 否决。当前 UI 由客户端拥有。实时呈现最终必须从持久消息派生,以使回放渲染出相同的记录。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- Fork 不改变源会话,创建的子会话具有平衡的已完成轮次前缀、`parentSession`、`seedLength`,以及逐字节一致的系统提示词。
|
||||
- 顾问定位在子会话追加历史的头部恰好添加一条插件来源的 `context/message`,而非修改其系统提示词。
|
||||
- 合并回写恰好添加一条有长度上限的 `context/message`,来源为 `plugin: sidechat`;父会话的下一次请求与回放在相同位置看到它。
|
||||
- 父会话与子会话并发运行,日志和流之间无串扰。
|
||||
- 单元测试覆盖 fork/attach 与合并回写;快照覆盖率随首个绑定界面一起落地。
|
||||
|
||||
## 风险
|
||||
|
||||
- 只读行为在 `tools/pre-execute` 拒绝门禁强制执行之前仅为建议性质;[拦截 seam](../../implemented/feature/2026-06-30-interception-seams.md) 可在不改变本机制的前提下添加该门禁。
|
||||
- 经过压缩(compaction)的源会话 fork 出的是其压缩视图,因此绑定的界面应当告知用户子会话继承的是摘要而非被替换的轮次。
|
||||
- 反复的 handback 会消耗父会话上下文。每次合并的长度上限约束了单条笔记的大小;后续的合并整理属于上下文压缩的职责。
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-13-stream-workflow-progress-through-tool-calls.md: 668d38b38cab90f6ee9b613d1101d4d44302aa8a
|
||||
2026-07-13-stream-workflow-progress-through-tool-calls.zh.md: b0daba0c62c9b06f33f5e3c6319f90670941e762
|
||||
@@ -0,0 +1,43 @@
|
||||
# Agent Note: 通过工具调用流式传输工作流进度
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-13-stream-workflow-progress-through-tool-calls.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
工作流引擎有意为 run、phase、narration 和子 agent(智能体)进度发出成对的 `workflow/*` observation 事件,但目前没有生产消费方呈现这些事件。因此,编辑器在最终结果返回之前只显示一张 pending 状态的工作流工具卡片,尽管引擎已经报告了当前活跃的 phase、脚本日志内容以及哪些子 agent 已启动或已结束。[动态工作流决策](../../implemented/feature/2026-07-05-dynamic-workflows.md)明确将 ACP(Agent Client Protocol)进度 UI 保留给这一事件流。
|
||||
|
||||
如果让 `dsh-acp` 直接监听工作流事件,就会反转能力边界:通用的 UI 桥接层将依赖一个可选的工作流包(package),并对一个工具名做特殊处理。工具流水线已经拥有实时更新所需的路由信息(agent 和 call id),但只暴露了纯粹的 pending/final 展示器,因此长时间运行的工具没有提供方无关的方式在二者之间报告瞬态 UI 状态。
|
||||
|
||||
## 提案
|
||||
|
||||
为 `dsh-tools` 添加一条实时进度通道。注册表所有的 `ToolExecution` 新增 `reportProgress(view): boolean`,其中 `view` 是一个独立的、提供方无关的通用进度快照,包含可选的替换标题和面向 UI 的内容块。进度不能更改调用的 args 派生卡片标签、kind、原始输入、locations、terminal intent 或 diff intent;它只更新在最初选定的展示方式内的实时标题/内容。当执行处于活跃状态时,该方法校验并快照 view,然后分发一个受限的、agent 作用域的 `tools/progress` observation,携带权威的执行标识与快照。一旦 final-result 处理开始,方法返回 `false` 且不再分发,因此迟到的异步报告者无法覆盖终态卡片。观察者异常会被记录日志,不会导致工具失败。
|
||||
|
||||
`dsh-acp` 以通用方式消费 `tools/progress`。它通过既有的 agent 到会话映射解析执行所属的 agent,并为同一 call id 发出 in-progress 的 `tool_call_update`。由于报告仅在工具执行流水线内可用,持久化的 `tool/call` 及其 ACP `tool_call` 始终先于第一条 update;在 `tools/result` 之前关闭报告者,确保进度更新不会出现在 completed/failed 卡片之后。进度是实时 UI 状态,而非模型输入或持久历史:会话回放继续从 `tool/call` 和 `tool/result` 重建 pending 与 final 卡片,无需重放瞬态更新。
|
||||
|
||||
`dsh-tool-workflow` 成为第一个生产者。每次工具执行在调用 `ctx.workflows.start()` 之前安装一个紧凑的事件捕获器,因为合法的引擎可能在 `start()` 内部同步发出进度。在调用返回之前,捕获器将观察到的事件按 `WorkflowRunInfo.id` 归约为候选状态;随后选取返回的 `WorkflowRun.id`,丢弃其他候选,报告累积的快照,并将后续匹配事件直接路由。如果 `start()` 抛出异常,捕获器被 dispose(资源释放),其候选状态被丢弃。这在不向 `WorkflowStartRequest` 添加观察者关联、也不要求进度等到 `start()` 返回的前提下,保持了引擎的可替换性。
|
||||
|
||||
归约器消费既有的 start、phase、log、agent-start、agent-end 和 end 事件,报告一个替换快照,包含当前 phase、最新日志行、活跃子 agent 标签以及 completed/failed/cancelled 计数。它不累积 narration transcript(文本记录);已结束的子 agent 离开活跃集合,变为计数器。`workflow/end`、工具结算或插件 dispose 移除归约器条目和事件捕获器。六种工作流事件及其元数据、成对的子 agent 生命周期、run handle、取消通道和观察者隔离保持不变;第三方观察者可继续直接消费这些事件。
|
||||
|
||||
更新工具执行/展示文档、生成的事件与 API 目录、工作流包文档以及工作流数据结构目录。ACP 集成覆盖率必须使用脚本化的模型边界测试真实的工作流工具和 worker seam;主 ACP 快照套件新增一个 workflow-progress 场景,因为这改变了面向编辑器的 transcript。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**删除工作流 observation 表面。** 在[折叠工作流简化提案](../../rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md)中被否决:这些事件及其成对生命周期是有意设计的,缺少的是消费方。
|
||||
|
||||
**让 ACP 直接了解工作流。** 这可以将 `WorkflowRunInfo` 映射到会话和卡片,但会使通用桥接层依赖一个可选能力,并绕过「工具拥有展示意图」的规则。工具进度通道为每个长时间运行的工具解决了相同的路由问题。
|
||||
|
||||
**将每条进度更新持久化为会话事件。** 这会使实时 narration 可回放,但会用一种状态永久膨胀日志,而该状态的权威持久结果已经是工具调用/结果对。如果可恢复的工作流进度成为产品需求,需要一个工作流日志化设计,而非伪装成持久事实的 UI 快照。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- `ToolExecution.reportProgress()` 由注册表所有、agent 作用域、快照化、观察者隔离,且在终态处理开始后返回 `false` 而不分发。
|
||||
- ACP 将进度路由到正确的实时会话中的正确调用;不同会话中的并发工作流不能串扰,且 `tool_call_update` 不会出现在其 `tool_call` 之前或终态更新之后。
|
||||
- 工作流进度显示当前 phase、最新日志行、活跃子 agent 和结果计数,同时保留所有既有 `workflow/*` 事件和 run 语义;一个在 `start()` 内部同步发出 start、phase、log、child 和 end 事件的 seam 测试引擎不会丢失任何归约器状态。
|
||||
- 取消、worker 死亡、工具失败、会话关闭和插件 dispose 释放归约器状态;回放仅发出持久的 pending/final 卡片对。
|
||||
- 单元测试、工作流集成测试、ACP 集成测试、快照、类型检查、覆盖率、doc-sync(文档同步门禁)、module-graph、构建和 hygiene 门禁全部通过。
|
||||
|
||||
## 风险
|
||||
|
||||
本提案向工具 seam 添加了一个公开的实时进度方法和事件,因此实现方必须精确维护 active/terminal 边界,并在观察者看到快照之前将其分离。pre-start 捕获器可能短暂观察到无关的工作流 run,因此它仅按 run id 持有紧凑的候选状态,并在 `start()` 返回后立即丢弃所有不匹配的候选。一个工作流可能发出大量进度变更;有界归约器避免了 transcript 增长,但在关联完成后仍会为每个有意义的事件发送一条 UI 更新。如果经测量的客户端需要合并更新,这必须是一个带默认值的、经过校验的桥接配置,而非硬编码的节流。瞬态进度在回放时有意消失,因此最终工具结果仍是唯一持久的工作流卡片内容。
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-06-11-api-extractor-reports.md: f110bfe3353e65442f218336aca3e9d492ac2341
|
||||
2026-06-11-api-extractor-reports.zh.md: 8cb7353e10a8811b20ccd539de15f8e06b76e6ae
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-06-11-api-extractor-reports.zh.md)
|
||||
|
||||
> Split out from the original "Doc-sync and API reports" Agent Note (2026-06-11). Parts 1-2 (doc-block typechecking, event-taxonomy verification) shipped — see [doc-sync enforcement](../../implemented/process/2026-06-11-doc-sync-enforcement.md). This is the deferred part 3, kept as a standalone proposal.
|
||||
|
||||
## Problem
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: API extractor 报告
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-06-11-api-extractor-reports.md) | 中文
|
||||
|
||||
> 从最初的「doc-sync(文档同步门禁)与 API 报告」Agent Note(agent 决策记录)中拆出(首次提出于 2026-06-11)。第 1 至第 2 部分(文档块类型检查、事件分类体系校验)已交付,见 [doc-sync 强制](../../implemented/process/2026-06-11-doc-sync-enforcement.md)。本文是被推迟的第 3 部分,作为独立提案保留。
|
||||
|
||||
## 问题
|
||||
|
||||
公开 API 的变更是不可见的:没有任何机制将「此次提交改变了公开接口」变为一个显式、可评审的事实。评审者阅读 diff 时可能遗漏某个导出类型新增了字段,或某个方法签名发生了变化。
|
||||
|
||||
## 提案
|
||||
|
||||
使用 api-extractor(或 `tsc --emitDeclarationOnly` 加一份规范化的公开接口导出)为每个包(package)生成一份签入仓库的 `etc/<pkg>.api.md`;CI 在重新生成结果与已签入报告不一致时失败。这样,每一次公开 API 变更都会成为评审者(或评审 agent(智能体))必须看到的一行 diff。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**`tsc --emitDeclarationOnly` 加规范化的公开接口导出**:如果 api-extractor 过于笨重,这是更轻量的机制;两者都能满足提案所需的「签入仓库、可 diff」的报告形态。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 每个包都有一份签入仓库的 `etc/<pkg>.api.md`;CI 在重新生成结果与已提交报告不一致时失败。
|
||||
- 公开 API 变更(新增导出、字段放宽、签名变化)在评审中以报告 diff 行的形式可见。
|
||||
|
||||
## 风险
|
||||
|
||||
该依赖笨重且难以调教(这正是它被推迟的原因),且报告格式会随编译器升级而变动,增加一个维护面;在各包尚未发布的阶段,收益有限。
|
||||
|
||||
## 推迟原因
|
||||
|
||||
在 doc-sync 落地时被推迟:对于一个内部 monorepo,评审者已经能看到源码 diff,价值不高;且依赖笨重、难以调教。如果各包将来对外发布,再重新评估——届时一份稳定、可 diff 的公开接口报告才值得其维护成本。
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-06-11-architectural-conformance.md: f7cb0d7397d4e03df225f68417da43b1fec8de62
|
||||
2026-06-11-architectural-conformance.zh.md: aa25ef6d2772642885ef268bd548fc6dad40d3cf
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-06-11-architectural-conformance.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Two architectural guarantees currently live only in prose: (1) nothing depends on the concrete loop package ([the microkernel promise](../../implemented/architecture/2026-06-11-microkernel-event-taxonomy.md)), and (2) every LlmAdapter speaks the chunk protocol correctly. Both should be mechanical ([the quality-gates principle](../../implemented/process/2026-06-11-quality-gates.md)).
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: 架构一致性——依赖规则与适配器套件
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-06-11-architectural-conformance.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
目前有两项架构保证仅存在于行文中:(1)没有任何组件依赖具体的 loop 包(package)([微内核承诺](../../implemented/architecture/2026-06-11-microkernel-event-taxonomy.md));(2)每个 LlmAdapter 都正确遵循分片协议。二者都应由机制强制执行([质量门禁原则](../../implemented/process/2026-06-11-quality-gates.md))。
|
||||
|
||||
## 提案
|
||||
|
||||
**dependency-cruiser** 配合以下规则:
|
||||
|
||||
- `packages/*`(除 agent-loop(智能体循环)自身的 tests 和 examples/ 外)禁止导入 `@deepseek-ai/dsh-agent-loop`。
|
||||
- 禁止跨包深层导入(`@deepseek-ai/dsh-*/src/...` 路径)——只允许使用公开入口点。
|
||||
- packages/ 内禁止导入循环。
|
||||
- `vendor/*` 禁止从 `packages/*` 导入。
|
||||
- 分层:dsh-llm 不导入其他 dsh 包;dsh-session 仅导入 dsh-llm;以此类推(packages/README.md 中的依赖表,强制执行)。
|
||||
|
||||
**适配器一致性套件**位于 dsh-llm(`@deepseek-ai/dsh-llm/conformance`):一个以适配器工厂为参数的可复用 vitest 套件,用于断言分片协议契约,包括每个块内的索引单调递增、某个索引出现 `block-end` 后不再接收增量、恰好出现一个 `finish`、用量至多出现一次、每个 `tool-call-delta` 都携带调用 id,并且及时响应 abort。当前先对 mock 运行;DeepSeek V4 适配器从第一天起继承该套件。还可以选择提供开发模式下的 `strictAdapter()` 包装层,在调试标志开启时于运行时强制执行相同规则(与 [开发模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md) 配对)。
|
||||
|
||||
## 计划
|
||||
|
||||
先落地 dependency-cruiser 配置与 CI 步骤(约一小时工作量,换来永久保证);一致性套件随其首个消费方测试(针对 MockAdapter)一起落地,并作为 V4 适配器阶段的前置条件。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- dependency-cruiser 在 CI 中运行上述规则族;违规导入导致构建失败。
|
||||
- 一致性套件对 mock 适配器和两个正式适配器运行,新适配器包通过调用该套件并传入自己的工厂即可继承测试。
|
||||
|
||||
## 风险
|
||||
|
||||
随着包的增加,dep-cruiser 规则需要维护——规则应基于模式(`dsh-*`)而非逐一枚举。
|
||||
|
||||
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-06-11-supply-chain-and-vendor-drift.md: a27ae64556dc7366279824f1480e5681b1e86bf1
|
||||
2026-06-11-supply-chain-and-vendor-drift.zh.md: 25c27650709faf1a462ce9779ee6f0a909746311
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-06-11-supply-chain-and-vendor-drift.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The vendor manifest ([the vendoring decision](../../implemented/process/2026-06-11-vendor-cordis-as-source.md)) is enforced at commit time in the *forward* direction (vendored change ⇒ manifest update) but nothing verifies the manifest's *claims*: that vendor/ actually equals upstream-at-SHA plus exactly the logged modifications. And the handful of true npm dependencies have no advisory monitoring or update cadence.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 供应链检查与 vendor 漂移验证
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-06-11-supply-chain-and-vendor-drift.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
vendor manifest(元数据清单)(见[引入 vendor 的决策](../../implemented/process/2026-06-11-vendor-cordis-as-source.md))在提交时仅在*正向*强制执行(vendor 变更 ⇒ manifest 更新),但没有任何机制验证 manifest 的*声明*:即 vendor/ 确实等于上游指定 SHA 的内容加上所记录的修改。此外,少量真正的 npm 依赖也没有安全公告监控或更新节奏。
|
||||
|
||||
## 提案
|
||||
|
||||
1. **Vendor 漂移检查**(夜间 CI):以 manifest 中记录的 SHA 浅克隆上游仓库,复制对应的包(package)源码,与 `vendor/*/src` 做 diff。除非 diff 与已记录的本地修改一致(每项修改以签入的 patch 文件保存——日志条目从行文描述变为可验证的产物),否则任务失败。
|
||||
2. **依赖安全公告**:对 lockfile 运行 osv-scanner(或 `pnpm audit`),按计划定期执行,并在涉及 lockfile 变更的 PR(Pull Request)上触发。
|
||||
3. **许可证清单**:一个脚本断言每个 vendor 包都携带其 LICENSE 文件,且 package.json 的 `license` 字段与 vendor/README.md 中的清单一致(我们混合了 vendor 的 MIT 与自有的 BSD-3)——作为 CI 步骤运行。
|
||||
4. **Renovate**(或定时 agent(智能体)任务)以小 PR 的形式提议 npm 依赖更新,这些 PR 走完整门禁套件;vendor 包不在其列(它们的更新遵循 manifest 同步流程,理想情况下是半自动化的 agent 工作流:拉取上游、重新应用 patch、运行门禁、以更新后的 manifest 表格开 PR)。
|
||||
|
||||
## 计划
|
||||
|
||||
第 3 项最简单,先做。第 1 项需要 CI 能通过网络访问上游仓库(私有仓库,需要 token),并将现有两项已记录的修改转换为 patch 文件。第 2 项和第 4 项是配置工作。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **用 `pnpm audit` 替代 osv-scanner**:两者都满足安全公告扫描的需求;具体选择推迟到实现阶段决定。
|
||||
- **用定时 agent 任务替代 Renovate**:在提议小型更新 PR 并走完整门禁套件方面效果等价;vendor 包无论哪种方案都不在其列(它们的更新遵循 manifest 同步流程)。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 许可证清单脚本在 CI 中运行,缺少 LICENSE 或 `license` 字段与 `vendor/README.md` 中的清单矛盾时失败。
|
||||
- 夜间漂移任务从 manifest SHA 加签入的 patch 文件重建 `vendor/`,出现任何无法解释的 diff 时失败。
|
||||
- 安全公告扫描按计划定期运行,并在涉及 lockfile 变更的 PR 上运行。
|
||||
|
||||
## 风险
|
||||
|
||||
上游仓库是私有镜像;CI 凭证与可用性是漂移检查的主要阻力。如果受阻,可改为本地定时 agent 任务而非 CI。
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-06-20-discover-package-inventory.md: 50e42686cea48985dedc7fa290e06bc959ecc5d4
|
||||
2026-06-20-discover-package-inventory.zh.md: dc79162dd3ec5ba2bf3225fc8e9d44c552a514cb
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-06-20-discover-package-inventory.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Package and gate inventories are repeated across TypeScript project references, package docs, CI prose, and Knip overrides. Most restate package layout, manifest data, or aggregate command contents. Each new package therefore creates avoidable synchronization points.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 通过发现机制获取包清单,而非维护静态列表
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-06-20-discover-package-inventory.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
包(package)与门禁清单在 TypeScript project references、包文档、CI 描述和 Knip 覆盖项中反复出现。大多数只是重述包布局、manifest(元数据清单)数据或聚合命令内容。因此每新增一个包都会产生本可避免的同步点。
|
||||
|
||||
[包层级结构](../../implemented/architecture/2026-06-20-package-hierarchy.md)已经手动消除了其中若干:`scripts/publint-all.ts` 现在从 `packages/<group>/<pkg>` 布局推导列表,两份 `tsconfig` 的 `paths` 映射也合并为一个 `@deepseek-ai/dsh-*` 通配符。剩下的是无法用 glob 消除的清单,主要是聚合配置(`tsconfig.host.json`、`tsconfig.client.json`)的 project `references`——TypeScript 要求它们是显式数组(没有通配符形式)。
|
||||
|
||||
当静态列表编码的是策略时,它们是合理的;当它们只是重复 `package.json`、workspace glob 或包层级结构中已有的 manifest 数据或布局事实时,就是不必要的摩擦。
|
||||
|
||||
## 提案
|
||||
|
||||
让剩余的包与门禁清单可被发现。一个唯一的权威来源,即 `packages/<group>/<pkg>` 层级结构加上包 manifest,应当驱动聚合配置的 `references`、模块图以及任何全量包列表,并配合一个生成加校验步骤(沿用现有的 `gen-module-graph` / `gen-cordis-catalog` 模式:生成器写出产物,`--check` 模式在 `hygiene` / `doc-sync`(文档同步门禁)中发现已提交副本陈旧时失败)。模块图生成已经在读取包 manifest。`doc-sync` 应当成为定义并打印其子门禁的唯一命令,文档链接到该命令,而非重述第二份列表。
|
||||
|
||||
层级结构不需要编码关于包的所有事实,但应当编码宽泛的维护策略:core/product 包、集成包、能力 seam 包与 support/test/example 包不应在脚本能区分它们之前先要求一份手工维护的例外列表。
|
||||
|
||||
有一项已编目的内容根本不需要生成器:将 e2e 入口 glob 折入 Knip 的默认配置段,即可直接删除逐包的重复声明。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 聚合配置的 project `references` 由层级结构生成(生成器输出它们;`--check` 门禁在提交副本陈旧时报错),而非手工维护。
|
||||
- 新增一个包时,不需要为任何门禁编辑静态包列表。
|
||||
- 文档描述真源,而非重复生成的清单。
|
||||
- CI 调用聚合命令,由这些命令自行管理其子门禁列表。
|
||||
- `knip.json` 仅在编码真实信息(额外入口文件、被忽略的依赖)时才携带逐包覆盖项,绝不重述默认配置段。
|
||||
|
||||
## 风险
|
||||
|
||||
发现脚本可能变得过于精巧。实现应当保持朴素:读取 manifest、按显式字段过滤、打印解析后的列表,并在出错时明确失败。收益在于消除手工清单的漂移,而非发明一套构建系统。
|
||||
|
||||
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-04-prune-dead-core-spine-surface.md: 432aaa848540fe7d1db3039399345a02afed5bd3
|
||||
2026-07-04-prune-dead-core-spine-surface.zh.md: 2c7cc4c0dc234afc107bdfc715cf717e5ad8c55c
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-07-04-prune-dead-core-spine-surface.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Several package-root exports, result fields, and convenience methods have no production consumer. They survive because tests import internals through public entry points or because a type anticipated a caller that never arrived. Each item is small in isolation, but together they enlarge the SDK contract, generated catalogs, documentation, and regression matrix without enabling a shipped path.
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Agent Note: 裁剪无用的公开与结果接口
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-04-prune-dead-core-spine-surface.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
若干包(package)根导出、结果字段和便利方法没有生产消费方。它们之所以存活,要么是因为测试通过公开入口导入了内部实现,要么是因为某个类型预期了一个从未出现的调用者。每一项单独看都很小,但合在一起,它们扩大了 SDK 契约、生成的 catalog、文档和回归矩阵,却没有支撑任何已交付的路径。
|
||||
|
||||
生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、包 README 和 Agent Note(agent 决策记录)行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/cordis/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此 catalog 中的服务方法和返回形状是真正的动态产品接口。下表因此区分「没有固定的仓库调用者」与「不可达」:涉及 catalog 词汇的行有意收缩模型编写的 mount 能发现和调用的内容,而包根实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单:
|
||||
|
||||
| 接口 | 生产证据 | 简化方式 |
|
||||
| --- | --- | --- |
|
||||
| `SurfaceManager.invalidate()` | 只有其单元测试调用它;seeding 在惰性创建的 manager 存在之前就已完成,且会话从不替换其日志引用。 | 删除它及其不可能触发的整体替换契约。 |
|
||||
| `ToolExecutionResult.callId` | 每个钩子已经接收不可变的 `ToolExecution`;循环和 ACP(Agent Client Protocol)通过调用/会话事件关联。没有消费方读取这个重复的结果字段。 | 移除该字段、复制/不匹配守卫,以及证明该重复不可能不一致的测试。 |
|
||||
| `ReactLoopAgent` 根导出 | 包外的命名导入都是测试;生产代码面向 `Agent` 编程,通过 `ctx.agents` 创建/恢复。 | 返回/接口类型为 `Agent`,将具体循环类改为包内部;保留有意设计的同步、仅配置的 `AgentLoop.create()` 路径。 |
|
||||
| `workflow-workerthread` 的 protocol/runtime/session 再导出与命名的 `WorkerWorkflowEngine` | 每个包名消费方都使用默认引擎;工作流 Agent Note 已将 worker 协议格式(wire format)定义为私有。 | 保留默认插件类/配置契约;移除重复的命名类导出,将协议模块保持为源码私有。 |
|
||||
| `code-runtime-worker` 的 protocol/bootstrap 再导出 | 包外的生产/e2e 消费方使用 `WorkerCodeRuntime` 和配置,而非 `BootstrapPort`、`PatchableStream` 或 worker 消息/启动类型。 | 保留运行时类/配置契约,将其协议格式/bootstrap 词汇改为源码私有。 |
|
||||
| ACP 的 translation/presenter 根导出 | `agentOptions`、`streamSessionEventUpdate`、`todosToPlan`、`ToolPresenter`、`nullToolPresenter` 和 `TerminalRendering` 只有同文件或 ACP 测试消费方;唯一的包外生产消费方挂载的是插件命名空间。 | 保留 `name`、`inject`、`Config`、`AcpConfig` 和 `apply`;将 translation/presentation 辅助函数改为源码私有,在包内测试。 |
|
||||
| `providerWording` 与 `completedTurnPrefix` 根导出 | 各有一个同包生产调用者;只有 balanced-prefix 辅助函数有一个同包白盒测试。 | 改为源码私有,测试提供方行为。 |
|
||||
| `depthOf`、`SubagentDepthError`、`SENSITIVE_ENV_PATTERN`、`waitForExit` 与 `exitsWithin` 根导出 | 生产 subagent 后端消费的是进程内 runner 和子进程构造/dispose(资源释放)辅助函数,而非这些强制/测试内部实现。 | 保留深度/环境/退出行为,但将辅助函数和 error/regex 改为源码私有;通过 spawn 和 dispose 测试。 |
|
||||
| `PersistenceCoordinator.inits`、后端 `inits` 访问器、`seedCoversPrefix` 与 `assertSerializable` | 访问器为白盒测试而存在;`seedCoversPrefix` 没有包外生产导入者;`assertSerializable` 没有生产调用者,且与 coordinator append 边界的无损快照重复。 | 通过 `session/flush` 观察初始化,将 `seedCoversPrefix` 改为源码私有,删除 `assertSerializable`。保留两个后端、`SessionHeader` 和 SQLite 的版本契约。 |
|
||||
| `LlmError.status` 与回放 status | 适配器/回放填充它,但生产分支基于稳定的错误码/消息判断,从不读取原始 status。 | 移除未读字段和回放管道,保留错误分类。 |
|
||||
| `BlockAssembler.push()` 返回值 | 两个生产调用者都忽略返回的已完成块。 | 返回 `void`;保留有意公开的 `blocks()`/`message()` 契约。 |
|
||||
| `compactRegion` 的独立 `session` 参数 | 固定调用者传入的对象与 `agent.session` 上已有的是同一个;模型可见的 mount API 也能调用该方法,但接受两个身份允许挂载的插件提供不一致的配对。 | 保留手动 region seam,同时有意将其收窄为以 `agent.session` 为唯一真源。 |
|
||||
| `CompactionResult.startSeq`、`summarySeq`、`endSeq` 与 `summary` | 生产消费方只读取 shadowed range/seq/token 统计;持久日志拥有 summary 和事件标识。 | 移除四个结果回显,保留两个共享的 transcript(文本记录)渲染器。 |
|
||||
| `BasicCompactService` 的 estimation/summarization 可见性 | 没有包外生产调用者调用这五个方法;已实现的 Agent Note 只将 `estimateContentTokens()` 和 `summarize()` 命名为子类钩子。 | 将这两个方法改为 `protected`,其余三个编排专用的估算器改为 private。 |
|
||||
| `CodeLogEntry.source`/`level` 与 `RunCodeMeta.dispatches` | 每个生产消费方都将日志映射为文本;没有 presenter/模型路径读取其他字段或持久化的 dispatch 计数。 | 将 code-runtime 日志改为字符串(或纯文本条目),移除 result-meta 的 dispatch 管道;保留用于生成确定性 dispatch id 的本地计数器。 |
|
||||
| `CodeRuntime.language` 与 `CodeRuntime.isolation` | worker 后端提供唯一的生产值,而 Code Mode 及其他所有生产调用方只调用 `run()`。 | 移除未读描述符,同时保留 worker 的语言、隔离、预算、取消与资源释放行为。 |
|
||||
| `ToolNotFoundError.toolName`、`SystemPrompt.config` 与 `BashTask.command` | 每个存储的公开值都没有生产读取者。 | 移除未读字段,保留错误消息、已解析的配置行为和任务生命周期。 |
|
||||
| 后端包根实现辅助函数 | 下方精确清单仅通过相对路径的同包导入调用。生产命名空间导入挂载的是保留的插件契约,不读取这些属性;命名根消费方都是测试。 | 保留每个适配器/提供方/服务及其配置/错误契约;停止在包根导出所列辅助函数/常量。 |
|
||||
| 消费方包根实现辅助函数 | 下方精确清单只有同包生产调用者。生产命名空间导入挂载的是插件契约,不读取辅助属性;命名根消费方都是测试。 | 保留插件契约和稳定的错误码;将测试迁移到包内模块或公开行为,停止在包根导出所列辅助函数。 |
|
||||
|
||||
### 分组辅助导出清单
|
||||
|
||||
- `dsh-llm-deepseek`:`httpErrorCode`、`serializeMessages`、`serializeRequest`、`DONE`、`parseSse`、`mapFinishReason`、`mapUsage` 与 `translate`;`dsh-llm-pi-ai`:`buildModel`、`mapStopReason`、`mapUsage`、`toPiContext` 与 `toStreamChunks`。
|
||||
- `dsh-bash-local`:`DEFAULT_GRACE_MS`、`ENV_OVERRIDES`、`killGroup`、`OutputCollector` 与 `runBash`;`dsh-bash-sandbox`:`shellQuote`、`classifyDenial` 与 `classifyRunnerFailure`;`dsh-sandbox-local`:`bwrapProfileArgs`、`landlockProfileArgs` 与 `seatbeltProfileArgs`。公开的可变测试注入字段及其类型不在本提案范围内。
|
||||
- `dsh-fs-local`:`applyLiteralEdit`、`listDirectory`、`probe`、`readForEdit`、`readTextForDiff`、`readWholeText`、`resolveLocalTarget`、`restoreLineEndings`、`streamWholeText` 与 `writeFileAtomic`。
|
||||
- `dsh-web-fetch-local`:`classifyContentType`、`decoderForCharset`、`isSameOrigin`、`parseCharset` 与 `validateFetchUrl`;`dsh-web-search-exa`:`mapExaResponse` 与 `mapExaResult`;`dsh-web-search-deepseek`:`citationSnippets` 与 `mapAnthropicResponse`;`dsh-web-search-perplexity`:`mapPerplexityResponse` 与 `mapPerplexityResult`。
|
||||
- `dsh-tool-fs`:`READ_LIMIT`、`STREAM_MIN_SIZE`、`READ_MAX_BYTES`、`READ_MAX_LINE_LENGTH`、`DIFF_CONTEXT`、`applyReadTool`、`parseReadArgs`、`applyWriteTool`、`formatWriteOutput`、`parseWriteArgs`、`applyEditTool`、`formatEditOutput`、`parseEditArgs`、`buildWindow`、`formatReadOutput`、`computeHunkDiffs` 与 `diffsFromMeta`。
|
||||
- `dsh-tool-web`:`WEB_SEARCH_MAX_RESULTS`、`applyWebSearchTool`、`formatSearchOutput`、`parseSearchArgs`、`presentSearchCall`、`applyWebFetchTool`、`formatFetchOutput`、`parseFetchArgs`、`presentFetchCall`、`renderBody` 与 `htmlToMarkdown`;`dsh-timeout-policy`:`toolTimeoutResult`;`dsh-compact-basic`:`resolveConfig`;`dsh-tool-bash`:`renderResult`。
|
||||
|
||||
## 提案
|
||||
|
||||
以一次有界的、协调的公开接口清理,移除或降级上述每一行。同步更新包 README、JSDoc、生成的 API/事件 catalog、type-equiv 记录、必要的 exports map 以及测试,使测试通过所属的公开 seam 验证行为,而非保留仅为测试而存在的入口。不折叠任何能力 seam、LLM(大语言模型)适配器、持久化后端或生命周期完全停稳契约。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留测试便利函数和自包含的结果字段为公开。** 公开辅助函数可以让白盒测试更方便,自包含的结果字段看起来更易用,未来的嵌入者可能需要具体循环类或枚举方法。这些好处是假设性的;当前它们让每处实现和文档都要解释没有已交付调用者能观察到的状态。真正的消费方可以引入它所需的最小契约,其所有权和失败语义明确。
|
||||
|
||||
**保留所有 catalog 成员以供模型编写的 mount 使用。** 自引用工具集是一条真实的通用消费路径,而非生成文档的噪音。然而,它的价值来自准确、可组合的服务接口,而非无限期保留重复字段或不一致的参数对;上述每一项 catalog 收缩都移除了在同一次执行、同一个 agent(智能体)或同一结果中其他位置已可获得的事实,并在同一变更中更新 API 参考。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 精确符号搜索显示:在本 Agent Note 及任何已实现 Agent Note 修正之外,没有被移除的接口。
|
||||
- 本 Agent Note 列出的每个接口均按指定方式缺失或降级;清单之外有意保留的扩展/测试契约不变。
|
||||
- 工具执行、上下文压缩(context compaction)、两个 LLM 适配器、两个持久化后端、工作流隔离以及 agent 创建/恢复保持其已交付行为。
|
||||
- 类型检查、覆盖率、快照、doc-sync(文档同步门禁)、module-graph 校验、构建和 hygiene 通过。
|
||||
|
||||
## 风险
|
||||
|
||||
大多数移除在编译时可见但对运行时无影响。上下文压缩参数清理有意禁止会话/上下文不匹配,同时保留手动 region seam。外部预发布嵌入者和现有模型编写的 mount 可能导入更少的辅助函数、传递更少的参数或接收更窄的结果形状;这是有意的产品接口收缩,而非仅仅是生成 catalog 的清理。仓库尚未发布,因此承载不受支持的接口才是更大的基础成本。
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-06-11-deterministic-and-stress-testing.md: d9977be835af05f9ee303b63ec6015bc9e153170
|
||||
2026-06-11-deterministic-and-stress-testing.zh.md: eff9eecb699344dff388bafec270f6b6677f71ee
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-06-11-deterministic-and-stress-testing.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Several loop tests synchronize with `setTimeout(30)` sleeps — flakiness debt that wastes agent cycles on retries and can mask ordering bugs. Separately, our core architectural promise (any session log replays to identical derived history) is asserted in two tests but is cheap to assert *everywhere*. And the inbox wakeup race was verified by hand exactly once; nothing re-verifies it continuously.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: 确定性测试、回放不变式 fixture(测试前置数据)与竞态压力测试
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-06-11-deterministic-and-stress-testing.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
若干 agent loop(智能体循环)测试通过 `setTimeout(30)` 睡眠来同步——这是一笔不稳定性债务,浪费 agent 的重试周期,还可能掩盖时序 bug。另外,我们的核心架构承诺(任何会话日志回放后都能得到相同的派生历史)目前只在两个测试中断言,但在*所有*测试中断言的成本极低。此外,inbox 唤醒竞态只被手动验证过一次,没有任何机制持续复验。
|
||||
|
||||
## 提案
|
||||
|
||||
三项措施:
|
||||
|
||||
1. **测试中禁止挂钟睡眠。** 将 `setTimeout(N)` 等待替换为事件驱动等待(既有的 `waitForIdle` 模式,扩展为 `waitForStatus`、`waitForEvent(n)`),或在需要测试时间本身时使用 vitest 的 fake timer。通过 lint 规则禁止 `setTimeout`,适用范围是 `packages/*/tests`,白名单辅助模块除外。
|
||||
2. **通用回放 fixture。** 一个共享测试辅助函数包装 agent loop harness,使每个测试结束后,agent 的会话日志被回放到一个全新的 Session 中,并自动断言 `deriveMessages()` 相等。这样该不变式在每次 CI 运行中会被套件产生的所有场景检查数百次,而非仅两次。
|
||||
3. **夜间竞态压力测试。** 一个 CI job 以 `vitest --repeat=200`(加 `--shuffle`)运行 agent-loop 和 inbox 套件,以暴露调度依赖的失败;发现的任何不稳定测试都视为 bug 修复,绝不靠重试掩盖。
|
||||
|
||||
## 计划
|
||||
|
||||
措施 1 和 2 一起落地(它们改动相同的辅助模块);在套件消除所有睡眠后再添加夜间 job,以确保重复运行速度快。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 不再使用 `setTimeout`;lint 规则在 `packages/*/tests` 中强制执行,白名单辅助模块除外。
|
||||
- 共享 harness 将每个测试的会话日志回放到全新的 `Session` 中,并自动断言 `deriveMessages()` 相等,覆盖整个套件。
|
||||
- 夜间 job 以 `--repeat` 和 `--shuffle` 运行 agent-loop 和 inbox 套件;发现的不稳定测试作为 bug 分诊,绝不靠重试掩盖。
|
||||
|
||||
## 风险
|
||||
|
||||
Fake timer 与 agent loop 中的 Promise 调度存在微妙交互——优先使用事件驱动等待;仅在测试 timer 服务行为本身时才使用 fake timer。
|
||||
|
||||
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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-06-11-mutation-testing.md: 591d9012644a19ee2c67a916b63092d79f78db1f
|
||||
2026-06-11-mutation-testing.zh.md: 9c22ed2f42e5c44e6be98f132614886bbdb188fd
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-06-11-mutation-testing.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The per-file 100% coverage gate ([the quality-gates decision](../../implemented/process/2026-06-11-quality-gates.md)) proves every line *executes* under test — not that any assertion would notice if the line were wrong. Under agent-written tests, coverage pressure can produce execution-without-assertion. Mutation testing measures what coverage cannot: whether the suite *kills* deliberately injected bugs.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: 变异测试作为覆盖率的制衡手段
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-06-11-mutation-testing.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
逐文件 100% 覆盖率门禁([质量门禁决策](../../implemented/process/2026-06-11-quality-gates.md))证明每一行代码在测试中都被*执行*了,但不能证明如果该行出错,任何断言会注意到。在 agent(智能体)编写测试的场景下,覆盖率压力可能产出「执行但不断言」的测试。变异测试衡量的正是覆盖率无法衡量的:测试套件是否能*杀死*被刻意注入的缺陷。
|
||||
|
||||
## 提案
|
||||
|
||||
使用 Stryker(`@stryker-mutator/vitest-runner`)对 `packages/*/src` 运行变异测试:
|
||||
|
||||
- **PR(Pull Request)范围的增量运行**(仅变更文件),作为一个 CI job。调优后速度足以作为合并门禁。
|
||||
- **每夜全量运行**,跟踪变异分数;先记录基线,再将阈值设为观测到的基线并只升不降(与覆盖率策略一致:阈值只收紧)。
|
||||
- 存活的变异体是待办项:agent 选取一个存活体、编写杀死它的测试、循环往复——一个形态良好的自主循环。
|
||||
- 等价变异体(可证明不改变行为的)加注释排除并附理由,与 `/* v8 ignore */` 策略一致。
|
||||
|
||||
## 计划
|
||||
|
||||
1. 添加 Stryker 配置,范围限定在一个包(package),即 llm(最小、最具算法性),并测量运行时间。
|
||||
2. 扩展到所有包;在配置中记录基线分数。
|
||||
3. 接入每夜 job;运行时间可接受后再添加 PR 范围的增量 job。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- Stryker 配置在 `packages/*/src` 上以 vitest runner 运行;每夜 job 记录变异分数,当分数低于记录的基线时,通过只升不降的阈值使运行失败。
|
||||
- PR 范围的增量运行在运行时间可接受后作为合并门禁;或者明确保持仅每夜运行,并将该结论记录于此。
|
||||
- 等价变异体带有注释排除及理由,与 `/* v8 ignore */` 策略一致。
|
||||
|
||||
## 风险
|
||||
|
||||
运行时间:变异测试开销大;逐文件 100% 覆盖率有所帮助(每个变异体至少会被执行到)。如果 PR 范围的运行始终过慢,则保持仅每夜运行,依赖分数只升不降的机制。
|
||||
|
||||
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->
|
||||
Reference in New Issue
Block a user