Merge branch 'feat/windows-pwsh-default' into feat/windows-acl-sandbox

# Conflicts:
#	packages/bash/pwsh-local/src/index.ts
#	packages/sandbox/sandbox-local/src/index.ts
This commit is contained in:
Huanqi Cao
2026-08-09 17:50:55 +08:00
1197 changed files with 3013 additions and 2907 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 .agents/notes/implemented/feature/2026-06-15-code-mode.md
2026-06-15-code-mode.md: 99bbed3edab32512f88ece9694d6519a1f89c2dd
2026-06-15-code-mode.zh.md: 87e39d7b68dd1dc2ebdee57f731be30d396a907b
2026-06-15-code-mode.md: 8a6fa068a4c7fee6fbfd9e16757a0fae5efa0c8b
2026-06-15-code-mode.zh.md: 00d4fd48b83e183d45423722bf223a63e305d5a4

View File

@@ -19,7 +19,7 @@ Tool presentation belongs to the registry that owns tool visibility: implementin
Three decisions, each elaborated in its own section below:
1. **Code Mode is a first-class presentation mode of `ToolRegistry`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'code'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry shapes its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation.
2. **Code execution is a capability seam** — `packages/code-runtime/` contains the interface package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another implementation package, not a redesign.
2. **Code execution is a capability seam** — `packages/code-runtime/` contains the Service Definition package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); Consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another Service provider package, not a redesign.
3. **The shipped implementation is `@deepseek-ai/dsh-code-runtime-worker`**: one fresh Node worker thread per run, executing the model's TypeScript after type-strip, with bindings bridged over the message port, an empty environment, configurable heap/output/time caps, and hard termination. Its trust posture is bash-equivalent by design — no unsafe-acknowledgement flags — because the harness already ships `dsh-bash-local`, which executes arbitrary model-written shell commands with strictly *more* ambient authority.
This note owns Code Mode's presentation, composition, isolation, and settlement foundation. The later [typed tool-return Agent Note](2026-07-20-code-mode-typed-tool-returns.md) owns the generated output map, canonical binding values, `ToolCallError`, and the lossless outer-output boundary.
@@ -118,7 +118,7 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem
## Risks
**The worker is not a hard security boundary.** Deliberate and documented (§Trust posture): posture equals the existing bash tool, containment exceeds it, gating uses the same seams. Deployments needing more need a future `isolation: 'container'` backend — tracked as the seam's designed extension, not a TODO on this design.
**The worker is not a hard security boundary.** Deliberate and documented (§Trust posture): posture equals the existing bash tool, containment exceeds it, gating uses the same approval and sandbox policies. Deployments needing more need a future `isolation: 'container'` backend — tracked as the seam's designed extension, not a TODO on this design.
**`stripTypeScriptTypes` is marked experimental.** It is the same engine (amaro/swc) behind Node's own native `.ts` execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite pins the behaviors relied on (position preservation, erasable-only rejection message shape loosely), the call sits behind one private function, and `amaro`/`sucrase` are drop-in replacements if the API shifts. The erasable-only subset is a model-facing contract line, and the error path is a working feedback loop, not a dead end.

View File

@@ -19,7 +19,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
三项决策,各自在下方独立小节中展开:
1. **Code Mode 是 `ToolRegistry`(`dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)、`'code'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)或 `'both'`(原生 schema 加传输通道 + SDK)。注册表在源头塑造其权威贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。
2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含接口包`@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`([能力 seam](../architecture/2026-06-13-capability-seams.md);消费方 = `dsh-tools`,core 消费 seam 的先例见 `agent-loop` → `dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个实现包,而非重新设计。
2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含 Service Definition 包 `@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`([能力 seam](../architecture/2026-06-13-capability-seams.md);Consumer = `dsh-tools`,core 消费 seam 的先例见 `agent-loop` → `dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个 Service provider 包,而非重新设计。
3. **交付的实现是 `@deepseek-ai/dsh-code-runtime-worker`**:每次运行 spawn 一个全新的 Node worker 线程,对模型的 TypeScript 进行 type-strip 后执行,绑定通过消息端口桥接,环境为空,堆/输出/时间上限可配置,并支持硬终止。其信任姿态在设计上等同于 bash——无需 unsafe-acknowledgement flag——因为 harness 已经交付了 `dsh-bash-local`,后者以严格*更高*的环境权限执行模型编写的任意 shell 命令。
本说明负责定义 Code Mode 的呈现、组合、隔离与结算基础。后续的[类型化工具返回值 Agent Note](2026-07-20-code-mode-typed-tool-returns.md)负责定义生成的输出映射、规范绑定值、`ToolCallError` 和无损外层输出边界。
@@ -118,7 +118,7 @@ SDK 指示模型编写一个所加载运行时语言的异步函数体(默认
## 风险
**Worker 不是硬安全边界。** 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,约束能力强于它,门禁使用相同的 seam。需要更强隔离的部署需要未来的 `isolation: 'container'` 后端——作为 seam 设计的扩展点跟踪,而非本设计的 TODO。
**Worker 不是硬安全边界。** 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,约束能力强于它,门禁使用相同的审批与沙箱策略。需要更强隔离的部署需要未来的 `isolation: 'container'` 后端——作为 seam 设计的扩展点跟踪,而非本设计的 TODO。
**`stripTypeScriptTypes` 标记为 experimental。** 它与 Node 自身原生 `.ts` 执行背后的引擎(amaro/swc)相同,在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件固定了所依赖的行为(位置保持、可擦除限制的拒绝消息形状宽松匹配),调用位于一个私有函数之后,且 `amaro`/`sucrase` 是 API 变化时的直接替代品。仅可擦除子集是面向模型的约定线,错误路径是一个可工作的反馈循环,而非死胡同。

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 .agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md
2026-06-17-filesystem-tool-schemas.md: 9941b3916b361a916c8148eb099eb8cfd46371c8
2026-06-17-filesystem-tool-schemas.zh.md: 8ba3c18629863ca9f04548d5993e9d200ecd4912
2026-06-17-filesystem-tool-schemas.md: b48f8e5d44913f2b6a2b7e1b19269c58e72efd1f
2026-06-17-filesystem-tool-schemas.zh.md: ad4081d28950a4a2df4f5718038e2a4124bf70b5

View File

@@ -20,7 +20,7 @@ The schema should be small enough to implement in the first `dsh-tool-fs` pass,
| `write` | `write(file_path, content)` | `Write(file_path, content)` | `write(content, filePath)` | Creates or overwrites UTF-8 text. Under the default fs-policy, updates to existing files require a prior observation; new-file creates do not. | YES |
| `edit` | `edit(file_path, old_string, new_string, replace_all?)` | `Edit(file_path, old_string, new_string, replace_all?)` | `edit(filePath, oldString, newString, replaceAll?)` | Literal string replacement; unique match required by default; under the default fs-policy requires a prior observation (any windowed read counts). | YES |
The schema uses snake_case field names (`file_path`, `old_string`, `new_string`, `replace_all`) to align with Claude Code and with existing DeepSeek Harness tool-schema examples. The consumer package translates these model-facing names into `ctx.fs` calls and `fs/*` event dispatches.
The schema uses snake_case field names (`file_path`, `old_string`, `new_string`, `replace_all`) to align with Claude Code and with existing DeepSeek Harness tool-schema examples. The Consumer package translates these model-facing names into `ctx.fs` calls and `fs/*` event dispatches.
## Tool schemas

View File

@@ -20,7 +20,7 @@ Status: implemented
| `write` | `write(file_path, content)` | `Write(file_path, content)` | `write(content, filePath)` | 创建或覆盖 UTF-8 文本。在默认 fs-policy 下,更新现有文件前必须先观测;创建新文件则不需要。 | 是 |
| `edit` | `edit(file_path, old_string, new_string, replace_all?)` | `Edit(file_path, old_string, new_string, replace_all?)` | `edit(filePath, oldString, newString, replaceAll?)` | 字面字符串替换;默认要求唯一匹配;在默认 fs-policy 下必须先观测(任意窗口读取均算作观测)。 | 是 |
schema 使用 snake_case 字段名(`file_path`、`old_string`、`new_string`、`replace_all`),与 Claude Code 及现有 DeepSeek Harness 工具 schema 示例保持一致。消费方包将这些面向模型的名称转换为 `ctx.fs` 调用和 `fs/*` 事件分发。
schema 使用 snake_case 字段名(`file_path`、`old_string`、`new_string`、`replace_all`),与 Claude Code 及现有 DeepSeek Harness 工具 schema 示例保持一致。Consumer 包将这些面向模型的名称转换为 `ctx.fs` 调用和 `fs/*` 事件分发。
## 工具 schema

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 .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md
2026-06-18-compaction-capability-seam.md: 390d091fd6e6f7fa1694ba78b90522b70948fe92
2026-06-18-compaction-capability-seam.zh.md: 297b9adda4f13f6fd4da9e73bf2bcf3d8d6ead0c
2026-06-18-compaction-capability-seam.md: 6357482600fabd0354325a19118ec245eee5d855
2026-06-18-compaction-capability-seam.zh.md: a5c60c667734f119a8bd9d9b6a8fbc9c66155470

View File

@@ -8,13 +8,13 @@ English | [中文](2026-06-18-compaction-capability-seam.zh.md)
A long-running agent conversation grows without bound. As the event log accumulates turns, the derived message history eventually approaches the model's context window — the model then truncates mid-response (`max-tokens`) or degrades. **Compaction** is the mitigation: replace a run of older history with a concise summary, keeping recent context intact.
The [session surface](../architecture/2026-06-18-session-surface.md) was built as the foundation for exactly this — an ordered projection over the event log with a `surfaceOp: { op: 'replace', start, end }` operation purpose-built to shadow a range of entries and insert a replacement, with `sourceEventSeqs` recording provenance so the decision replays deterministically. What remained was the plugin that *decides what to compact and produces the summary*.
The [session surface](../architecture/2026-06-18-session-surface.md) was built as the foundation for exactly this — an ordered projection over the event log with a `surfaceOp: { op: 'replace', start, end }` operation purpose-built to shadow a range of entries and insert a replacement, with `sourceEventSeqs` listing every source event so replay can validate that the replacement cites every event it removes. What remained was the plugin that *decides what to compact and produces the summary*.
Two forces shape the design. First, compaction policy and reusable token measurement vary independently: measurement belongs to the LLM-family [`ctx.tokenMeter` service](../architecture/2026-07-15-replay-token-meter-service.md), while summarization can be a model call, a template, or a remote service. Second, `SurfaceEventType` is closed to the message-producing event types (`user/message`, `assistant/message`, `tool/result`); only those may carry `surfaceOp`. A bespoke `compaction/*` event therefore **cannot** itself appear on the surface — the compiler and Session's always-on append/seed boundary reject `surfaceOp` on it.
## Decision
### Compaction is a capability seam, split interface / implementation
### Compaction is a capability seam with separate Service Definition and Service provider roles
Per the [capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md), compaction ships as separate packages so the contract, the algorithm, and (later) the consumer surface evolve independently:
@@ -25,7 +25,7 @@ Per the [capability-seams Agent Note](../architecture/2026-06-13-capability-seam
### The contract depends on `dsh-session` and `dsh-llm` — a deliberate deviation
The capability-seams Agent Note states the interface package "depends only on cordis" (true of `dsh-bash`, whose vocabulary is self-contained). Compaction **cannot** honor that: its verbs act on an agent-owned `Session` (`compactRegion(start, end, agent)`) and its output uses the content vocabulary (`CompactionResult.summary: ContentBlock[]`). There is no way to express the contract without naming `Session`/`SessionEvent` (from `dsh-session`) and `ContentBlock` (from `dsh-llm`).
The capability-seams Agent Note states the Service Definition package "depends only on cordis" (true of `dsh-bash`, whose vocabulary is self-contained). Compaction **cannot** honor that: its verbs act on an agent-owned `Session` (`compactRegion(start, end, agent)`) and its output uses the content vocabulary (`CompactionResult.summary: ContentBlock[]`). There is no way to express the contract without naming `Session`/`SessionEvent` (from `dsh-session`) and `ContentBlock` (from `dsh-llm`).
This is not a coupling smell — it is the contract's domain. The "only cordis" guidance was always shorthand for "the interface depends only on what the contract genuinely names, and never on an implementation." `dsh-session` and `dsh-llm` are themselves interface/vocabulary packages, not implementations; `dsh-compact` still imports no backend. The seam's real invariant — *consumers and implementations evolve independently behind an abstract service* — holds intact.
@@ -71,12 +71,12 @@ Auto-compaction always starts at the surface head, merging the prior checkpoint
### Surface replacement: `compact/*` events are log-only; one `user/message` carries the summary
Because `SurfaceEventType` is closed, the summary cannot ride on a `compact/*` event. The backend instead appends a **single `user/message`** with `source: COMPACT_CHECKPOINT_SOURCE` and `surfaceOp: { op: 'replace', start, end }` whose `content` is the (framed) summary and whose `sourceEventSeqs` covers the shadowed entries *and* the bookkeeping events. The interface exports that source and `isCompactCheckpointSource()` so consumers recognize a persisted or cloned checkpoint without depending on backend package identity. The `compact/*` events are pure log records (lock + provenance). The surface mutation sits **inside** the lock — `compact/end` is the last event appended:
Because `SurfaceEventType` is closed, the summary cannot ride on a `compact/*` event. The backend instead appends a **single `user/message`** with `source: COMPACT_CHECKPOINT_SOURCE` and `surfaceOp: { op: 'replace', start, end }` whose `content` is the (framed) summary and whose `sourceEventSeqs` covers the shadowed entries *and* the bookkeeping events. The interface exports that source and `isCompactCheckpointSource()` so consumers recognize a persisted or cloned checkpoint without depending on backend package identity. The `compact/*` events record the lock, summary, selected range, shadowed seqs, token count, and model call without joining the surface. The surface mutation sits **inside** the lock — `compact/end` is the last event appended:
```
compact/start → log-only. Acquires the lock.
[summarize older range via the backend]
compact/summary → log-only. Provenance: raw summary, local-call marker, range, shadowed seqs, token count.
compact/summary → log-only. Records the raw summary, local-call marker, range, shadowed seqs, and token count.
user/message → canonical checkpoint source + surfaceOp { op:'replace', start, end }.
THE surface mutation (framed summary).
deriveMessages() renders it as a user-role message.
@@ -93,7 +93,7 @@ The basic backend wraps the summary as established checkpoint context and tags i
The `compact/start … compact/end` bracket is justified by two roles:
1. **Crash-detectable orphan + provenance** (primary). Summarization is a slow model call persisted *after* `compact/start`. A crash mid-summarization leaves a `compact/start` with no matching `compact/end` — a detectable orphan. Releasing the lock last (rather than first) converts the crash window from *silent corruption* into that detectable orphan.
1. **Crash-detectable orphan plus recorded summary inputs** (primary). Summarization is a slow model call persisted *after* `compact/start`. A crash mid-summarization leaves a `compact/start` with no matching `compact/end` — a detectable orphan. Releasing the lock last (rather than first) converts the crash window from *silent corruption* into that detectable orphan.
2. **Prevents concurrent compaction.** Every automatic, manual, and explicit-range entry point refuses a live unmatched `compact/start`. The bracket is the single lock; no process-local mutex duplicates it.
The lock excludes another compaction, not unrelated facts. Its markers are time points rather than an exclusive container, so durable inbox splices may appear between a standalone manual start and end. Automatic work requires whole-surface stability inside its turn. Manual work revalidates only the selected positional span, letting append-only context outside it remain visible after replacement.
@@ -119,10 +119,10 @@ The lifecycle boundary makes crash state unambiguous:
## Consequences
- **Packages**: `packages/compact/compact` supplies the interface, `compact-basic` supplies the backend, `compact-tool-result-prune` supplies optional deterministic rewriting, and `command-compact` supplies human `/compact`. `packages/llm/token-meter` owns replay-aware measurement independently.
- **Automatic seams**: `agent/pre-step` (`@mode waterfall`) handles pressure before request derivation and `agent/request-error` (`@mode waterfall`) handles final request failures after the failed step closes. The pre-step payload carries the claimed batch, turn, step, and signal (see the [payload-object events decision](../architecture/2026-08-06-agent-event-payload-objects.md)), with no compaction-only prompt/prefix payload.
- **Automatic extension points**: `agent/pre-step` (`@mode waterfall`) handles pressure before request derivation and `agent/request-error` (`@mode waterfall`) handles final request failures after the failed step closes. The pre-step payload carries the claimed batch, turn, step, and signal (see the [payload-object events decision](../architecture/2026-08-06-agent-event-payload-objects.md)), with no compaction-only prompt/prefix payload.
- **`SessionEventMap`** gains `compact/start` / `compact/summary` / `compact/end` by declaration merging (merge-extensible); `SurfaceEventType` is **not** touched. These are session events, not cordis `Events`, so the event-taxonomy gate needs no entry.
- **`dsh-compact`** owns `COMPACT_CHECKPOINT_SOURCE`, `isCompactCheckpointSource(source)`, `toolPairingBalancedBefore(session, seq)`, and `toolPairingBalancedAfter(session, seq)`. The marker identifies replacement summaries across backend implementations. The cached surface-edge checks prevent `compactRegion` and `compactIfNeeded` from splitting a tool-call/result pair, validate current membership by seq, answer both edges from one per-cut balance sequence, and reject stale or missing seqs and orphan results.
- **`dsh-session`** validates positional replacement, complete provenance, and content-only single-node `tool/result` rewrites through its one surface manager. Its invariant companion treats fresh appended tool results as executions that require an open step and pending call, while the compaction companion owns numeric-turn versus standalone-null bracket relations.
- **`dsh-session`** validates positional replacement, complete cited source-event coverage, and content-only single-node `tool/result` rewrites through its one surface manager. Its invariant companion treats fresh appended tool results as executions that require an open step and pending call, while the compaction companion owns numeric-turn versus standalone-null bracket relations.
- **Wiring**: `examples/tui-agent/cordis.yml` loads zero-config `dsh-token-meter`, `dsh-compact-tool-result-prune`, `dsh-compact-basic`, then `dsh-command-compact`; service-wide defaults make the composition usable without repeated numeric policy.
## Testing

View File

@@ -8,13 +8,13 @@ Status: implemented
长时间运行的 agent(智能体)对话会无限增长。随着事件日志不断累积轮次,派生出的消息历史最终逼近模型的上下文窗口,模型随即截断响应(`max-tokens`)或性能退化。**上下文压缩(context compaction)** 是对此的缓解手段:用一段简洁的摘要替换一批较早的历史,保持近期上下文完整。
[会话接口面](../architecture/2026-06-18-session-surface.md)正是为此而构建的基础设施:一份建立在事件日志之上的有序投影,带有专门设计的 `surfaceOp: { op: 'replace', start, end }` 操作,用于遮蔽一段条目并插入替换内容,`sourceEventSeqs` 记录溯源信息以便决策可确定性地回放。剩下的是那个*决定压缩什么、并产出摘要*的插件。
[会话接口面](../architecture/2026-06-18-session-surface.md)正是为此而构建的基础设施:一份建立在事件日志之上的有序投影,带有专门设计的 `surfaceOp: { op: 'replace', start, end }` 操作,用于遮蔽一段条目并插入替换内容,`sourceEventSeqs` 列出每个来源事件,使回放可以验证替换是否引用了它移除的每个事件。剩下的是那个*决定压缩什么、并产出摘要*的插件。
两股力量塑造了设计。第一,压缩策略与可复用的 token 测量独立变化:测量归 LLM 系列的 [`ctx.tokenMeter` 服务](../architecture/2026-07-15-replay-token-meter-service.md)所有,摘要生成则可以使用模型调用、模板或远程服务。第二,`SurfaceEventType` 封闭为产生消息的事件类型(`user/message`、`assistant/message`、`tool/result`);只有这些类型可以携带 `surfaceOp`。因此一个专用的 `compaction/*` 事件**不能**出现在 surface 上,编译器与 Session 始终启用的 append/seed 边界都会拒绝在其上附加 `surfaceOp`。
## 决策
### 压缩是一个能力 seam,接口与实现分离
### 压缩是一个能力 seam,Service Definition 与 Service provider 角色分离
遵循[能力 seam Agent Note](../architecture/2026-06-13-capability-seams.md),压缩以独立包发布,使约定、算法和(后续的)消费方 surface 各自独立演进:
@@ -25,7 +25,7 @@ Status: implemented
### 约定依赖 `dsh-session` 和 `dsh-llm`——有意为之的偏离
能力 seam Agent Note 规定接口包「仅依赖 cordis」(对 `dsh-bash` 成立,因为其词汇是自包含的)。压缩**无法**遵守这一点:它的动词作用于 agent 所有的 `Session`(`compactRegion(start, end, agent)`),其输出使用内容词汇(`CompactionResult.summary: ContentBlock[]`)。不引用 `Session`/`SessionEvent`(来自 `dsh-session`)和 `ContentBlock`(来自 `dsh-llm`),约定就无法表达。
能力 seam Agent Note 规定 Service Definition 包「仅依赖 cordis」(对 `dsh-bash` 成立,因为其词汇是自包含的)。压缩**无法**遵守这一点:它的动词作用于 agent 所有的 `Session`(`compactRegion(start, end, agent)`),其输出使用内容词汇(`CompactionResult.summary: ContentBlock[]`)。不引用 `Session`/`SessionEvent`(来自 `dsh-session`)和 `ContentBlock`(来自 `dsh-llm`),约定就无法表达。
这不是耦合异味,而是约定的领域所在。「仅 cordis」的指导原则一直是「接口仅依赖约定真正需要命名的东西,绝不依赖实现」的简写。`dsh-session` 和 `dsh-llm` 本身是接口/词汇包,不是实现;`dsh-compact` 仍然不导入任何后端。seam 的真正不变式——*消费方和实现在抽象服务背后独立演进*——完好无损。
@@ -33,7 +33,7 @@ Status: implemented
早期草案将完整算法(保留遍历、token 求和、文本提取)作为接口上的具体方法。这会将约定重新耦合到一种策略:想要不同保留策略或事件排序的后端必须与继承来的具体代码对抗。将三个操作都设为抽象,把所有*怎么做*的决策放在后端,并让接口保持为*做什么*的声明。token 测量根本不是压缩钩子;单例服务使多个消费方能够共享逐会话的回放折叠。
`compactIfNeeded(agent, trigger, signal)` 接受显式的 `'pressure' | 'context-overflow'` 触发原因与取消信号。它只读取最新的持久化已路由请求;没有 header 就不执行工作,任何已路由的提供方/模型目标都使用单例估算器。`compactNow(agent, signal)` 要求 agent 处于 idle,即使未达到压力也进行一次有效的平衡缩减;不存在这种范围时返回 `null`,且不写入任何内容。`compactRegion(start, end, agent, signal?)` 将 `agent.session` 作为唯一会话身份,并为显式调用方保留可选 signal。默认摘要器依次从显式配置、最新记录的已路由目标和 agent 选项解析目标,并在任何 `llm/stream` 路由后记录提供方/模型对。它回放已路由请求的前缀,并将压缩指令追加为尾部 user 消息,从而复用提供方的热 KV Cache;见[摘要前缀缓存 Agent Note](../bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md)。该结果携带 `llmStreamCall: true`,因为生成它时恰好通过此上下文的 LLM 服务发起了一次调用;只有满足相同条件时,子类才设置该标记,因为单有保留的 `rawOutput` 并不能判定调用路径。该调用将提供方无关的 `GenerateOptions.purpose` 设为 `compaction`;适配器可以将此用途映射为对模型隐藏的传输元数据,DeepSeek 适配器会发送 `x-deepseek-harness-compact: 1`。
`compactIfNeeded(agent, trigger, signal)` 接受显式的 `'pressure' | 'context-overflow'` 触发原因与取消信号。它只读取最新的持久化已路由请求;没有 header 就不执行工作,任何已路由的提供方/模型目标都使用单例估算器。`compactNow(agent, signal)` 要求 agent 处于 idle,即使未达到压力也进行一次有效的平衡缩减;不存在这种范围时返回 `null`,且不写入任何内容。`compactRegion(start, end, agent, signal?)` 将 `agent.session` 作为唯一会话身份,并为显式调用方保留可选 signal。默认摘要器依次从显式配置、最新记录的已路由目标和 agent 选项解析目标,并在任何 `llm/stream` 路由后记录提供方/模型对。它回放已路由请求的前缀,并将压缩指令追加为尾部 user 消息,从而复用提供方的热 KV cache;见[摘要前缀缓存 Agent Note](../bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md)。该结果携带 `llmStreamCall: true`,因为生成它时恰好通过此上下文的 LLM 服务发起了一次调用;只有满足相同条件时,子类才设置该标记,因为单有保留的 `rawOutput` 并不能判定调用路径。该调用将提供方无关的 `GenerateOptions.purpose` 设为 `compaction`;适配器可以将此用途映射为对模型隐藏的传输元数据,DeepSeek 适配器会发送 `x-deepseek-harness-compact: 1`。
### 成功的持久步骤工作完成后运行自动压力检查
@@ -71,12 +71,12 @@ retry → next numbered step/start ⟵ derives from the replacement surface
### Surface 替换:`compact/*` 事件仅存在于日志;一条 `user/message` 承载摘要
由于 `SurfaceEventType` 是封闭的,摘要不能搭载在 `compact/*` 事件上。后端改为追加一条**单独的 `user/message`**,带有 `source: COMPACT_CHECKPOINT_SOURCE` 和 `surfaceOp: { op: 'replace', start, end }`;其 `content` 是(带框架的)摘要,`sourceEventSeqs` 覆盖被遮蔽的条目*和*簿记事件。接口导出该来源和 `isCompactCheckpointSource()`,使消费方无需依赖后端包身份,即可识别持久化或克隆得到的检查点。`compact/*` 事件是纯日志记录(锁 + 溯源信息)。surface 变更位于锁**内部**,`compact/end` 是最后追加的事件:
由于 `SurfaceEventType` 是封闭的,摘要不能搭载在 `compact/*` 事件上。后端改为追加一条**单独的 `user/message`**,带有 `source: COMPACT_CHECKPOINT_SOURCE` 和 `surfaceOp: { op: 'replace', start, end }`;其 `content` 是(带框架的)摘要,`sourceEventSeqs` 覆盖被遮蔽的条目*和*簿记事件。接口导出该来源和 `isCompactCheckpointSource()`,使消费方无需依赖后端包身份,即可识别持久化或克隆得到的检查点。`compact/*` 事件记录锁、摘要、选中区间、被遮蔽的 seq、token 数和模型调用,但不加入 surface。surface 变更位于锁**内部**,`compact/end` 是最后追加的事件:
```
compact/start → log-only. Acquires the lock.
[summarize older range via the backend]
compact/summary → log-only. Provenance: raw summary, local-call marker, range, shadowed seqs, token count.
compact/summary → log-only. Records the raw summary, local-call marker, range, shadowed seqs, and token count.
user/message → canonical checkpoint source + surfaceOp { op:'replace', start, end }.
THE surface mutation (framed summary).
deriveMessages() renders it as a user-role message.
@@ -93,7 +93,7 @@ compact/end → log-only. Releases the lock (carries `error` on a recoverab
`compact/start … compact/end` 标记对承担两项职责:
1. **可检测的崩溃孤儿 + 来源追溯**(首要)。摘要生成是一次慢速模型调用,持久化在 `compact/start` *之后*。摘要生成中途崩溃会留下一个没有匹配 `compact/end` 的 `compact/start`——一个可检测的孤儿。最后释放锁(而非最先)将崩溃窗口从*静默损坏*转变为可检测的孤儿。
1. **可检测的崩溃孤儿 + 已记录的摘要输入**(首要)。摘要生成是一次慢速模型调用,持久化在 `compact/start` *之后*。摘要生成中途崩溃会留下一个没有匹配 `compact/end` 的 `compact/start`——一个可检测的孤儿。最后释放锁(而非最先)将崩溃窗口从*静默损坏*转变为可检测的孤儿。
2. **防止并发压缩。** 每个自动、手动和显式范围入口点都会拒绝活动的未匹配 `compact/start`。该标记对就是唯一的锁;没有进程本地 mutex 重复承担同一职责。
该锁只排除另一项压缩,不排除无关事实。其标记是时间点,而不是排他的容器,因此持久 inbox splice 可以出现在独立手动 start 与 end 之间。自动工作要求其轮次内的整个 surface 保持稳定。手动工作只重新验证所选位置 span,使其外部的仅追加上下文在替换后保持可见。
@@ -119,10 +119,10 @@ compact/end → log-only. Releases the lock (carries `error` on a recoverab
## 后果
- **包**:`packages/compact/compact` 提供接口,`compact-basic` 提供后端,`compact-tool-result-prune` 提供可选的确定性重写,`command-compact` 提供面向用户的 `/compact`。`packages/llm/token-meter` 独立拥有回放感知的测量。
- **自动 seam**:`agent/pre-step`(`@mode waterfall`)在请求派生前处理压力,`agent/request-error`(`@mode waterfall`)处理失败步骤关闭后的最终请求失败。pre-step 的 payload 携带已领取批次、轮次、步骤与 signal(参见 [payload-object 事件决策](../architecture/2026-08-06-agent-event-payload-objects.md)),不携带压缩专属的提示词/前缀 payload。
- **自动扩展点**:`agent/pre-step`(`@mode waterfall`)在请求派生前处理压力,`agent/request-error`(`@mode waterfall`)处理失败步骤关闭后的最终请求失败。pre-step 的 payload 携带已领取批次、轮次、步骤与 signal(参见 [payload-object 事件决策](../architecture/2026-08-06-agent-event-payload-objects.md)),不携带压缩专属的提示词/前缀 payload。
- **`SessionEventMap`** 通过可合并扩展的声明合并获得 `compact/start` / `compact/summary` / `compact/end`;`SurfaceEventType` **未被**触及。这些是会话事件,不是 cordis `Events`,因此事件分类门禁无需新增条目。
- **`dsh-compact`** 拥有 `COMPACT_CHECKPOINT_SOURCE`、`isCompactCheckpointSource(source)`、`toolPairingBalancedBefore(session, seq)` 与 `toolPairingBalancedAfter(session, seq)`。该标记用于跨后端实现识别替换摘要。带缓存的 surface 边缘检查会防止 `compactRegion` 和 `compactIfNeeded` 拆分工具调用/结果对,按 seq 校验当前成员关系,从每个切割点的一条平衡序列回答两侧边缘,并拒绝陈旧或缺失的 seq 与孤立结果。
- **`dsh-session`** 通过唯一的 surface 管理器校验位置替换、完整溯源信息和仅内容的单节点 `tool/result` 重写。其不变式配套插件将新追加的工具结果视为执行,要求存在已打开的步骤与待处理调用,而压缩配套组件拥有数字轮次归属与独立 `null` 归属标记对之间的关系。
- **`dsh-session`** 通过唯一的 surface 管理器校验位置替换、引用的来源事件是否覆盖完整,以及仅内容的单节点 `tool/result` 重写。其不变式配套插件将新追加的工具结果视为执行,要求存在已打开的步骤与待处理调用,而压缩配套组件拥有数字轮次归属与独立 `null` 归属标记对之间的关系。
- **接线**:`examples/tui-agent/cordis.yml` 依次加载零配置的 `dsh-token-meter`、`dsh-compact-tool-result-prune`、`dsh-compact-basic`,然后加载 `dsh-command-compact`;服务级默认值使组合无需重复数值策略即可使用。
## 测试

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 .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
2026-06-21-subagent-capability-seam.md: 752e639b09ea2ac0ba19841ddfe38b15b44d22e9
2026-06-21-subagent-capability-seam.zh.md: f0b0ba2ab8b0bb7b611e995c56e30db68b21cfa7
2026-06-21-subagent-capability-seam.md: 553411a0d8f989c883e3eea929669ccebcfe807b
2026-06-21-subagent-capability-seam.zh.md: 5fa2bd62828e9afcb79647ec4821c2da1da041e7

View File

@@ -14,18 +14,18 @@ The distinctive requirement — the one that shapes the whole design — is that
- **in-process** — a child concrete `Agent` on the same `Context` (the cheapest, and nearly free given the existing agent factory);
- **ACP** — act as an ACP *client* driving another agent process (which can be another instance of ourselves);
- **Codex app-server and Claude Code Agent SDK** — current one-shot siblings that apply the same named-provider seam to official product processes ([product-provider Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md));
- **Codex app-server and Claude Code Agent SDK** — current one-shot siblings that apply the same named-provider contract to official product processes ([product-provider Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md));
- later: **A2A** using the same out-of-process "start a child, prompt it, settle, cancel" shape.
## Alternatives considered
### Why not the bash seam shape
The bash seam ([capability seams](../architecture/2026-06-13-capability-seams.md)) registers exactly one `BashExecutor` per context; loading a second throws. That is correct for bash (one machine, one way to run a command) but wrong here: coexistence is the requirement. So the subagent service is a **named-provider registry** — each implementation registers under a unique name and a caller picks one by name — mirroring the **LLM adapter registry** (`LlmService.registerAdapter`), not the single-service bash executor. The seam is still three-package (interface / implementation / consumer); only the "one vs. many implementations" axis differs.
The bash seam ([capability seams](../architecture/2026-06-13-capability-seams.md)) registers exactly one `BashExecutor` per context; loading a second throws. That is correct for bash (one machine, one way to run a command) but wrong here: coexistence is the requirement. So the subagent service is a **named-provider registry** — each implementation registers under a unique name and a caller picks one by name — mirroring the **LLM adapter registry** (`LlmService.registerAdapter`), not the single-service bash executor. The seam is still three-package (Service Definition / Service provider / Consumer); only the "one vs. many implementations" axis differs.
## Decision
### The three-package seam
### The three-package boundary
A new package group `packages/subagent/`:

View File

@@ -14,18 +14,18 @@ harness 有一个长期搁置的 seam 用于 **subagent**:一个 agent 将工
- **进程内**:在同一个 `Context` 上创建一个具体的子 `Agent`(最廉价,且鉴于现有 agent 工厂几乎零成本);
- **ACP**:作为 ACP *客户端*驱动另一个 agent 进程(可以是自身的另一个实例);
- **Codex app-server 与 Claude Code Agent SDK**:当前的一次性同类提供方,将同一个命名提供方 seam 应用于官方产品进程([产品提供方 Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md));
- **Codex app-server 与 Claude Code Agent SDK**:当前的一次性同类提供方,将同一个命名提供方约定应用于官方产品进程([产品提供方 Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md));
- 后续:**A2A**,采用同样的进程外形态:「启动子 agent、发送提示词、结算、取消」。
## 曾考虑的替代方案
### 为何不采用 bash seam 的形状
bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在每个上下文中只注册恰好一个 `BashExecutor`;加载第二个会抛异常。这对 bash 是正确的(一台机器、一种执行命令的方式),但对这里是错误的:共存才是需求。因此 subagent 服务是一个**命名提供方注册表**——每个实现以唯一名称注册,调用方按名称选择——镜像 **LLM(大语言模型)适配器注册表**(`LlmService.registerAdapter`),而非单服务的 bash 执行器。seam 仍然是由三个包构成的结构(接口、实现、消费方);只是「一个 vs. 多个实现」这个维度不同。
bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在每个上下文中只注册恰好一个 `BashExecutor`;加载第二个会抛异常。这对 bash 是正确的(一台机器、一种执行命令的方式),但对这里是错误的:共存才是需求。因此 subagent 服务是一个**命名提供方注册表**——每个实现以唯一名称注册,调用方按名称选择——镜像 **LLM(大语言模型)适配器注册表**(`LlmService.registerAdapter`),而非单服务的 bash 执行器。seam 仍然是由三个包构成的结构(Service Definition / Service provider / Consumer);只是「一个 vs. 多个实现」这个维度不同。
## 决策
### 由三个包构成的 seam
### 由三个包构成的边界
新建包组 `packages/subagent/`:

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 .agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md
2026-06-22-acp-subagent-backend.md: c994ebfa69649bb9e79d3aa389a0e13c178131c7
2026-06-22-acp-subagent-backend.zh.md: 79be03dfc8310f6fc759d620c36d0d279c703073
2026-06-22-acp-subagent-backend.md: 211030a6ecc9b6f673174ba410415c7bd80818ef
2026-06-22-acp-subagent-backend.zh.md: 2dde3d96329cc47d9c7cca08e1d25764c32f8ebe

View File

@@ -59,4 +59,4 @@ Every run pays a fresh subprocess (spawn + `initialize` + `newSession`). The par
## Product-provider siblings
The [Codex app-server and Claude Code Agent SDK providers](2026-08-04-claude-code-and-codex-subagent-backends.md) apply the same out-of-process spawn/prompt/settle/cancel boundary as siblings registered by name. A2A remains a future sibling transport; the ACP backend proves that the common seam supports this boundary without owning product-private protocols.
The [Codex app-server and Claude Code Agent SDK providers](2026-08-04-claude-code-and-codex-subagent-backends.md) apply the same out-of-process spawn/prompt/settle/cancel boundary as siblings registered by name. A2A remains a future sibling transport; the ACP backend proves that the subagent seam supports this boundary without owning product-private protocols.

View File

@@ -59,4 +59,4 @@ ACP `StopReason` → harness `SubagentStopReason`:`end_turn`→`completed`、`
## 兄弟产品提供方
[Codex app-server 与 Claude Code Agent SDK 提供方](2026-08-04-claude-code-and-codex-subagent-backends.md)作为按名称注册的兄弟提供方,采用同样的进程外启动/提示词/结算/取消边界。A2A 仍是未来的兄弟传输方式;ACP 后端证明了通用 seam 能够支持这项边界,而无需负责产品私有协议。
[Codex app-server 与 Claude Code Agent SDK 提供方](2026-08-04-claude-code-and-codex-subagent-backends.md)作为按名称注册的兄弟提供方,采用同样的进程外启动/提示词/结算/取消边界。A2A 仍是未来的兄弟传输方式;ACP 后端证明了 subagent seam 能够支持这项边界,而无需负责产品私有协议。

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 .agents/notes/implemented/feature/2026-06-30-hook-bridges.md
2026-06-30-hook-bridges.md: a31116818c57e18d04e656f371efd1101a63b6af
2026-06-30-hook-bridges.zh.md: 0fa76c0cee0f74101e70f44d442daa2fb46e6649
2026-06-30-hook-bridges.md: eecc1fc75c10618481231083f890e0e4b122c1ee
2026-06-30-hook-bridges.zh.md: dcc8f496cb3bc6ea7857ce804a9159cbfcbc3f08

View File

@@ -6,9 +6,9 @@ English | [中文](2026-06-30-hook-bridges.zh.md)
## Problem
The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/pre-step`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-stopping`, `subagent/start`, or `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)).
The harness's extension surface is its typed interception points ([the interception extension-points Agent Note](2026-06-30-interception-extension-points.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/pre-step`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-stopping`, `subagent/start`, or `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed extension points, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)).
The framing that shapes the whole design: **a bridge is a compatibility adapter, not a power tool.** Anything a bridge does (block a tool, inject context, force continuation, observe a subagent) a native cordis plugin does more powerfully — typed returns, full `ctx`, no serialization boundary. The bridge's reason to exist is to run the explicitly supported subset of external CC/Codex command hooks. That keeps each bridge thin: parse the config, pick a matcher mode, build the per-event payload, call `runHook` + `mergeHookOutputs` from the shared lib, and map the neutral outcome onto a seam Decision. The package READMEs own the exact current unsupported-event and partial-field inventory against the official protocols.
The framing that shapes the whole design: **a bridge is a compatibility adapter, not a power tool.** Anything a bridge does (block a tool, inject context, force continuation, observe a subagent) a native cordis plugin does more powerfully — typed returns, full `ctx`, no serialization boundary. The bridge's reason to exist is to run the explicitly supported subset of external CC/Codex command hooks. That keeps each bridge thin: parse the config, pick a matcher mode, build the per-event payload, call `runHook` + `mergeHookOutputs` from the shared lib, and map the neutral outcome to a typed Decision. The package READMEs own the exact current unsupported-event and partial-field inventory against the official protocols.
## Decision
@@ -19,9 +19,9 @@ Two independent plugins in the `packages/hooks/` group, each a function/namespac
### Outcome → Decision mapping
Each bridge maps the neutral `MergedHookOutcome` from the shared lib onto the seam's typed Decision:
Each bridge maps the neutral `MergedHookOutcome` from the shared lib onto each extension point's typed Decision:
| Seam | CC | Codex |
| Extension point | CC | Codex |
|---|---|---|
| `agent/session-start` (emit) | additionalContext → `agent.inject()` | plain-stdout output → additionalContext → `agent.inject()` |
| `agent/pre-step` | `deny`→`reject`; context-only→delegate+fold into `enter` | `block`→`reject`; context-only→delegate+fold into `enter` |
@@ -59,9 +59,9 @@ Hooks run in the agent's session workspace, so relative paths target the user's
- **Tool-input rewrite.** A CC/Codex `updatedInput` is logged + warned, not honored — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite Agent Note](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)), because the pre-execution args are read by `tool/call` audit + `assistant/message` history + tool presentation, so an honest rewrite is a design unit, not a field.
- **Stop loop-guard** (`TODO(stop-loop-guard)`). Claude Code supplies `stop_hook_active` and overrides a hook after eight consecutive blocks; Codex supplies `stop_hook_active` but documents no equivalent cap. Both bridges always report `false`, so a Stop hook that unconditionally blocks force-continues every step — a hook author must self-limit until state tracking lands.
- **Hook `continue:false` (hard halt).** A hook can ask to halt the whole run (CC/Codex `continue:false`); the shared merge folds it into `MergedHookOutcome.stop`/`stopReason`, but no bridge acts on it (`TODO(hook-continue-false)`) — the interception seams have no "hard-halt the agent" primitive yet (a Decision blocks/steers a single point, not the run). Deferred with the loop-guard work; mid-turn requests record the halt in `hook/result`, and the hook keeps its per-point effect (decision/context) meanwhile.
- **Hook `continue:false` (hard halt).** A hook can ask to halt the whole run (CC/Codex `continue:false`); the shared merge folds it into `MergedHookOutcome.stop`/`stopReason`, but no bridge acts on it (`TODO(hook-continue-false)`) — the interception points have no "hard-halt the agent" primitive yet (a Decision blocks/steers a single point, not the run). Deferred with the loop-guard work; mid-turn requests record the halt in `hook/result`, and the hook keeps its per-point effect (decision/context) meanwhile.
- **Config discovery.** The path is explicit in `cordis.yml` and process-level (see above); the full multi-layer CC/Codex precedence walk, per-session project-local discovery, and the trust/hash model are not reimplemented (`TODO(per-session-hook-config)`).
- **Session-start / subagent-start context is best-effort (`TODO(session-start-gating)`).** Both hooks run detached from startup, so their context is injected when ready but may miss the first request or a short-lived child. Guaranteeing first-request delivery requires an awaited startup seam.
- **Session-start / subagent-start context is best-effort (`TODO(session-start-gating)`).** Both hooks run detached from startup, so their context is injected when ready but may miss the first request or a short-lived child. Guaranteeing first-request delivery requires an awaited startup extension point.
## Alternatives considered

View File

@@ -6,9 +6,9 @@ Status: implemented
## 问题
harness 的扩展面是其类型化的拦截 seam(见[拦截 seam Agent Note](2026-06-30-interception-seams.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start`、`agent/pre-step`、`tools/pre-execute`、`tools/post-execute`、`agent/turn-stopping`、`subagent/start` 或 `subagent/end`。但用户带着**既有的** Claude Code(CC)和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 Agent Note 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化 seam 上,构建于共享的协议格式(wire format)库之上(见 [hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md))。
harness 的扩展面是其类型化拦截点(见[拦截扩展点 Agent Note](2026-06-30-interception-extension-points.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start`、`agent/pre-step`、`tools/pre-execute`、`tools/post-execute`、`agent/turn-stopping`、`subagent/start` 或 `subagent/end`。但用户带着**既有的** Claude Code(CC)和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 Agent Note 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化扩展点上,构建于共享的协议格式(wire format)库之上(见 [hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md))。
贯穿整个设计的定位:**桥接是兼容性适配器,不是高级工具。** 桥接能做的事(阻止工具、注入上下文、强制继续、观察 subagent),原生 Cordis 插件都能做得更强——类型化返回值、完整 `ctx`、无序列化边界。桥接存在的理由是运行外部 CC/Codex 命令钩子中被明确支持的子集。这使每个桥接保持精简:解析配置、选择匹配模式、构建每事件的 payload、调用共享库的 `runHook` + `mergeHookOutputs`,再将中性结果映射为 seam Decision。各包的 README 维护着当前不支持的事件和部分字段的完整清单,以官方协议为参照。
贯穿整个设计的定位:**桥接是兼容性适配器,不是高级工具。** 桥接能做的事(阻止工具、注入上下文、强制继续、观察 subagent),原生 Cordis 插件都能做得更强——类型化返回值、完整 `ctx`、无序列化边界。桥接存在的理由是运行外部 CC/Codex 命令钩子中被明确支持的子集。这使每个桥接保持精简:解析配置、选择匹配模式、构建每事件的 payload、调用共享库的 `runHook` + `mergeHookOutputs`,再将中性结果映射为类型化 Decision。各包的 README 维护着当前不支持的事件和部分字段的完整清单,以官方协议为参照。
## 决策
@@ -19,9 +19,9 @@ harness 的扩展面是其类型化的拦截 seam(见[拦截 seam Agent Note](
### Outcome → Decision 映射
每个桥接将共享库返回的中性 `MergedHookOutcome` 映射到 seam 的类型化 Decision:
每个桥接将共享库返回的中性 `MergedHookOutcome` 映射到各扩展点的类型化 Decision:
| Seam | CC | Codex |
| 扩展点 | CC | Codex |
|---|---|---|
| `agent/session-start`(emit) | additionalContext → `agent.inject()` | 纯 stdout 输出 → additionalContext → `agent.inject()` |
| `agent/pre-step` | `deny`→`reject`;仅上下文→委托并折叠到 `enter` | `block`→`reject`;仅上下文→委托并折叠到 `enter` |
@@ -59,9 +59,9 @@ Claude Code 始终导出 `CLAUDE_PROJECT_DIR`,常见的未修改钩子引用 `
- **工具输入重写。** CC/Codex 的 `updatedInput` 被记录日志并发出警告,但不予执行——输入重写是一个推迟的一致性设计问题(见 [pre-tool-input-rewrite Agent Note](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)),因为 pre-execution 参数被 `tool/call` 审计、`assistant/message` 历史和工具展示共同读取,诚实的重写是一个设计单元,而非一个字段。
- **Stop 循环防护**(`TODO(stop-loop-guard)`)。Claude Code 提供 `stop_hook_active` 并在连续八次阻塞后覆盖钩子;Codex 提供 `stop_hook_active` 但未记录等效上限。两个桥接始终报告 `false`,因此一个无条件阻塞的 Stop 钩子会在每一步强制继续——在状态追踪落地之前,钩子作者必须自行限制。
- **钩子 `continue:false`(硬停止)。** 钩子可以请求终止整个运行(CC/Codex `continue:false`);共享合并将其折叠为 `MergedHookOutcome.stop`/`stopReason`,但没有桥接对其采取行动(`TODO(hook-continue-false)`)——拦截 seam 尚无「硬停止 agent」原语(Decision 阻塞/引导的是单个点,而非整个运行)。与循环防护工作一同推迟;轮中请求会将停止请求记录在 `hook/result` 中,钩子在此期间保留其逐点效果(决策/上下文)。
- **钩子 `continue:false`(硬停止)。** 钩子可以请求终止整个运行(CC/Codex `continue:false`);共享合并将其折叠为 `MergedHookOutcome.stop`/`stopReason`,但没有桥接对其采取行动(`TODO(hook-continue-false)`)——拦截点尚无「硬停止 agent」原语(Decision 阻塞/引导的是单个点,而非整个运行)。与循环防护工作一同推迟;轮中请求会将停止请求记录在 `hook/result` 中,钩子在此期间保留其逐点效果(决策/上下文)。
- **配置发现。** 路径在 `cordis.yml` 中显式指定且为进程级(见上文);完整的多层 CC/Codex 优先级遍历、按会话的项目本地发现以及信任/hash 模型未被重新实现(`TODO(per-session-hook-config)`)。
- **Session-start / subagent-start 上下文为尽力而为(`TODO(session-start-gating)`)。** 两个钩子以 detached 方式运行,不阻塞启动流程,因此其上下文在就绪时注入,但可能错过首个请求或短命的 subagent。要保证首请求送达,需要一个 awaited 的启动 seam。
- **Session-start / subagent-start 上下文为尽力而为(`TODO(session-start-gating)`)。** 两个钩子以 detached 方式运行,不阻塞启动流程,因此其上下文在就绪时注入,但可能错过首个请求或短命的 subagent。要保证首请求送达,需要一个 awaited 的启动扩展点。
## 曾考虑的替代方案

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 .agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md
2026-06-30-hook-protocol-lib.md: 2edff0d501cd7695873c68054eeb3a1e9942eced
2026-06-30-hook-protocol-lib.zh.md: 90648c535e8bccd264ef9ae1ffd5f66590b1ac6d
2026-06-30-hook-protocol-lib.md: 576a126e1022ac72265c476e03ef36aaf17f78b4
2026-06-30-hook-protocol-lib.zh.md: f2554eaa378a26bf8254d86d458e0de5ce1aa4fd

View File

@@ -21,7 +21,7 @@ A new `packages/hooks/` group with `hook-protocol` as a pure library. It owns fo
- **Merge** — `mergeHookOutputs(outputs)`, folding multiple matched hooks into one most-restrictive `MergedHookOutcome`: permission precedence **deny > ask > allow**, halt sticky on the first `continue:false`, block reasons joined `\n\n`, context/system-messages accumulated in order.
- **`hook/*` session events** — `hook/invoked` / `hook/result`, declaration-merged into `SessionEventMap` (log-only, like `compact/*` — NOT `SurfaceEventType`s), with `appendHookInvoked`/`appendHookResult` helpers so the invoked/result pairing and owner-defined execution relation stay consistent across bridges. `appendHookResult` also owns the durable record's semantics — the decision string (the hook's parsed decision, else `'stop'` on `continue:false`, else `'pass'`) and the 500-character `stderrSummary` truncation derive from the `HookOutput` here, not per-bridge.
**Per-dialect (the bridge plugins):** building each event's stdin payload (CC's base+per-event field sets vs Codex's snake_case with `turn_id`/`model` extras), the dialect's env + `${CLAUDE_PLUGIN_ROOT}` substitution (CC) vs none (Codex), and mapping the neutral `HookOutput`/`MergedHookOutcome` onto the harness's seam-specific typed Decisions (`PreToolDecision`, `PreStepDecision`, `ContinuationDecision`, `PostToolDecision`).
**Per-dialect (the bridge plugins):** building each event's stdin payload (CC's base+per-event field sets vs Codex's snake_case with `turn_id`/`model` extras), the dialect's env + `${CLAUDE_PLUGIN_ROOT}` substitution (CC) vs none (Codex), and mapping the neutral `HookOutput`/`MergedHookOutcome` onto the harness's extension-point-specific typed Decisions (`PreToolDecision`, `PreStepDecision`, `ContinuationDecision`, `PostToolDecision`).
## Alternatives considered

View File

@@ -21,7 +21,7 @@ Status: implemented
- **合并** — `mergeHookOutputs(outputs)`,将多个匹配钩子的输出折叠为一个最严格的 `MergedHookOutcome`:权限优先级 **deny > ask > allow**,halt 在首个 `continue:false` 时粘滞,阻止原因以 `\n\n` 拼接,context/system-messages 按序累积。
- **`hook/*` 会话事件** — `hook/invoked` / `hook/result`,通过声明合并进入 `SessionEventMap`(仅日志,如 `compact/*`——不是 `SurfaceEventType`),配有 `appendHookInvoked`/`appendHookResult` 辅助函数,确保 invoked/result 配对与由所有者定义的执行关系在各桥接插件间保持一致。`appendHookResult` 还负责定义持久化记录的语义:decision 字符串(钩子解析出的 decision,否则 `continue:false` 时为 `'stop'`,否则为 `'pass'`)和 500 字符的 `stderrSummary` 截断均从本库的 `HookOutput` 派生,而非各桥接插件各自实现。
**方言专属(桥接插件):** 构建每个事件的 stdin payload(CC 的 base+per-event 字段集 vs Codex 的 snake_case 加 `turn_id`/`model` 额外字段)、方言的 env 与 `${CLAUDE_PLUGIN_ROOT}` 替换(CC)vs 两者皆无(Codex),以及将方言无关的 `HookOutput`/`MergedHookOutcome` 映射为 harness seam 专属的类型化 Decision(`PreToolDecision`、`PreStepDecision`、`ContinuationDecision`、`PostToolDecision`)。
**方言专属(桥接插件):** 构建每个事件的 stdin payload(CC 的 base+per-event 字段集 vs Codex 的 snake_case 加 `turn_id`/`model` 额外字段)、方言的 env 与 `${CLAUDE_PLUGIN_ROOT}` 替换(CC)vs 无替换(Codex),以及将方言无关的 `HookOutput`/`MergedHookOutcome` 映射为 harness 各扩展点专属的类型化 Decision(`PreToolDecision`、`PreStepDecision`、`ContinuationDecision`、`PostToolDecision`)。
## 曾考虑的替代方案

View File

@@ -1,6 +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 .agents/notes/implemented/feature/2026-06-30-interception-seams.md
2026-06-30-interception-seams.md: 3b2c62cd4413f6d93cef247c51304a8d46753218
2026-06-30-interception-seams.zh.md: 110b28d48a342b50f8e5c1024ed2f2df704e4099
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-interception-extension-points.md
2026-06-30-interception-extension-points.md: 68be3c94f55ae13155e9b22cb91ef1ad10b1e0ed
2026-06-30-interception-extension-points.zh.md: 262793dd7a297f343391a22fb8d9f1d57a9ff531

View File

@@ -1,18 +1,18 @@
# Agent Note: Interception seams — the typed-Decision surface a hook programs against
# Agent Note: Interception extension points — the typed-Decision surface a hook programs against
Status: implemented
English | [中文](2026-06-30-interception-seams.zh.md)
English | [中文](2026-06-30-interception-extension-points.zh.md)
## Problem
The harness needs a hooks subsystem: users extend or gate the agent at lifecycle points the way Claude Code (CC) and Codex do. The key reframe driving this design is that **"native hooks" are not a package** — a native hook is just an ordinary Cordis plugin subscribing to the canonical lifecycle events. So the real product is a *powerful, well-typed canonical event surface*; the CC/Codex bridges (the `dsh-hooks-claude` / `dsh-hooks-codex` packages) are merely translators that map an external shell-hook protocol onto that same surface. Anything a bridge can do, a plain plugin can do directly — more powerfully (no serialization boundary, full `ctx`, typed returns).
The surface needs distinct contracts for per-prompt policy (CC's `UserPromptSubmit`), session-start observation (CC's `SessionStart`), pre-tool policy, around-dispatch control, post-tool transformation, final-result observation, and continuation with a model-facing reason. Conflating those phases gives plugins mutation channels they do not need and makes finality depend on listener ordering. The [event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md) supplies the three-domain rule and the typed-Decision idiom; this Agent Note applies them to the lifecycle seams.
The surface needs distinct contracts for per-prompt policy (CC's `UserPromptSubmit`), session-start observation (CC's `SessionStart`), pre-tool policy, around-dispatch control, post-tool transformation, final-result observation, and continuation with a model-facing reason. Conflating those phases gives plugins mutation channels they do not need and makes finality depend on listener ordering. The [event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md) supplies the three-domain rule and the typed-Decision idiom; this Agent Note applies them to the lifecycle extension points.
## Decision
The canonical surface separates transformable policy, around-dispatch control, and observe-only notification. Policy waterfalls return small seam-specific **typed Decision unions**; wrappers return normalized results; notifications receive immutable snapshots and cannot affect the outcome. The set covers the hook points in scope (`session-start`, `prompt-submit`, `pre-tool`, `post-tool`, `stop`-via-continuation) while leaving non-hook execution policy independently composable.
The canonical surface separates transformable policy, around-dispatch control, and observe-only notification. Policy waterfalls return small extension-point-specific **typed Decision unions**; wrappers return normalized results; notifications receive immutable snapshots and cannot affect the outcome. The set covers the hook points in scope (`session-start`, `prompt-submit`, `pre-tool`, `post-tool`, `stop`-via-continuation) while leaving non-hook execution policy independently composable.
**Agent events** (`dsh-agent`):
- `agent/session-start({ agent, source })` — emit, once before turn 1, carrying a `SessionStartSource` (`startup` for a fresh/forked create, `resume` for a reloaded persisted session; `clear`/`compact` reserved). A pure notification — it CANNOT block startup (a deliberate gap: a bridge logs/injects, it does not gate startup). A listener seeds context via `agent.inject()`.
@@ -47,12 +47,12 @@ Core dispatch and the tool body sit inside normalization boundaries, so tool, li
### Boundaries
The seam package does **not** declare `hook/*` session events (the durable hook-invocation log); those belong to `dsh-hook-protocol`, because a native plugin uses typed decisions without an external hook log. The native-plugin integration test (`packages/core/agent-loop/tests/interception.spec.ts`) composes the seams through the real loop with no `hook/*` protocol. Compaction (`PreCompact`/`PostCompact`), Notification, and Codex `PermissionRequest` remain outside this decision. The [approval seam](2026-07-06-approval-seam.md) resolves `ask` decisions through `ctx.approval`; terminal monotonic stopping is expressed by tool-result data, while `agent/turn-stopping` is the last chance to steer another step.
The Service Definition package does **not** declare `hook/*` session events (the durable hook-invocation log); those belong to `dsh-hook-protocol`, because a native plugin uses typed decisions without an external hook log. The native-plugin integration test (`packages/core/agent-loop/tests/interception.spec.ts`) composes the extension points through the real loop with no `hook/*` protocol. Compaction (`PreCompact`/`PostCompact`), Notification, and Codex `PermissionRequest` remain outside this decision. The [approval seam](2026-07-06-approval-seam.md) resolves `ask` decisions through `ctx.approval`; terminal monotonic stopping is expressed by tool-result data, while `agent/turn-stopping` is the last chance to steer another step.
## Alternatives considered
- **Shipping pre-tool INPUT rewrite as part of this seam set** — deferred as the over-reach signal; the section above carries the consistency problem (audit, history, and presentation all read `tool/call.arguments` logged before execution), and [the pre-tool input-rewrite proposal](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md) owns the design.
- **Declaring the durable `hook/*` SessionEvents alongside the seams** — rejected: a native plugin uses the typed Decisions with no hook log at all (the worked example proves it), so the durable log belongs to [the hook-protocol library](2026-06-30-hook-protocol-lib.md), not the seam surface.
- **Shipping pre-tool INPUT rewrite as part of this extension-point set** — deferred as the over-reach signal; the section above carries the consistency problem (audit, history, and presentation all read `tool/call.arguments` logged before execution), and [the pre-tool input-rewrite proposal](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md) owns the design.
- **Declaring the durable `hook/*` SessionEvents alongside the extension points** — rejected: a native plugin uses the typed Decisions with no hook log at all (the worked example proves it), so the durable log belongs to [the hook-protocol library](2026-06-30-hook-protocol-lib.md), not the extension surface.
## Consequences

View File

@@ -1,22 +1,22 @@
# Agent Note: 拦截 seam——钩子编程所面对的类型化 Decision 表面
# Agent Note: 拦截扩展点——钩子编程所面对的类型化 Decision 表面
Status: implemented
[English](2026-06-30-interception-seams.md) | 中文
[English](2026-06-30-interception-extension-points.md) | 中文
## 问题
harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那样在生命周期节点扩展或管控 agent(智能体)。驱动本设计的关键视角转换是:**「原生钩子」不是一个包**——原生钩子只是一个普通的 Cordis 插件,订阅规范的生命周期事件。因此真正的产品是一个*强大、类型完备的规范事件表面*;CC/Codex 桥接(`dsh-hooks-claude` / `dsh-hooks-codex` 包)只是将外部 shell 钩子协议映射到同一表面的翻译层。桥接能做的事,普通插件可以直接做——而且更强大(无序列化边界、完整 `ctx`、类型化返回值)。
该表面需要为以下场景提供各自独立的约定:逐提示词策略(CC 的 `UserPromptSubmit`)、会话启动观测(CC 的 `SessionStart`)、工具执行前策略、环绕调度控制、工具执行后变换、最终结果观测,以及携带面向模型的原因的继续执行。如果把这些阶段混为一谈,插件就会获得不需要的 mutation 通道,而终结性将依赖监听器的注册顺序。[事件域语义 Agent Note](../architecture/2026-06-30-event-domain-semantics.md) 提供了三域规则与类型化 Decision 惯用法;本 Agent Note 将其应用于生命周期 seam。
该表面需要为以下场景提供各自独立的约定:逐提示词策略(CC 的 `UserPromptSubmit`)、会话启动观测(CC 的 `SessionStart`)、工具执行前策略、环绕调度控制、工具执行后变换、最终结果观测,以及携带面向模型的原因的继续执行。如果把这些阶段混为一谈,插件就会获得不需要的 mutation 通道,而终结性将依赖监听器的注册顺序。[事件域语义 Agent Note](../architecture/2026-06-30-event-domain-semantics.md) 提供了三域规则与类型化 Decision 惯用法;本 Agent Note 将其应用于生命周期扩展点。
## 决策
规范表面将可变换策略、环绕调度控制与仅观测通知分离。策略 waterfall(瀑布式事件)返回小型的、seam 专属的**类型化 Decision 联合类型**;包装层返回规范化结果;通知接收不可变快照,无法影响结果。覆盖的钩子点包括 `session-start`、`prompt-submit`、`pre-tool`、`post-tool`、通过 continuation 实现的 `stop`,同时将非钩子的执行策略留作独立可组合。
规范表面将可变换策略、环绕调度控制与仅观测通知分离。策略 waterfall(瀑布式事件)返回小型的、扩展点专属的**类型化 Decision 联合类型**;包装层返回规范化结果;通知接收不可变快照,无法影响结果。覆盖的钩子点包括 `session-start`、`prompt-submit`、`pre-tool`、`post-tool`、通过 continuation 实现的 `stop`,同时将非钩子的执行策略留作独立可组合。
**Agent 事件**(`dsh-agent`):
- `agent/session-start({ agent, source })` ——emit,在第 1 轮次之前触发一次,携带 `SessionStartSource`(`startup` 表示全新/fork 创建,`resume` 表示重新加载的持久化会话;`clear`/`compact` 保留)。纯通知,不能阻塞启动(这是有意的空白:桥接可以记录/注入,但不管控启动)。监听器通过 `agent.inject()` 注入上下文。
- `agent/pre-step({ agent, messages, turn, step, signal }, next) → PreStepDecision` ——waterfall,在每个拟议步骤之前、循环原子移除其独占 inbox 批次后触发。payload 携带该请求的 `turn`、`step` 与取消 `signal`(已退役的 `PreStepContext` 字段位于 payload 中;参见 [payload-object 事件决策](../architecture/2026-08-06-agent-event-payload-objects.md));没有中途输入的工具续步中,`messages` 为空。`enter` 返回完整消息批次,其中包括监听器为当前请求贡献的上下文;`reject` 不打开步骤,并让已领取消息保持移除状态。
- `agent/pre-step({ agent, messages, turn, step, signal }, next) → PreStepDecision` ——waterfall,在每个拟议步骤之前、循环原子移除其独占 inbox 批次后触发。payload 携带该请求的 `turn`、`step` 与取消 `signal`(已退役的 `PreStepContext` 字段位于 payload 中;参见 [payload-object 事件决策](../architecture/2026-08-06-agent-event-payload-objects.md));没有中途输入的工具续步会收到空批次。`enter` 返回完整消息批次,其中包括监听器为当前请求贡献的上下文;`reject` 不打开步骤,并让已领取消息保持已删除。
**`agent/turn-stopping`** 是自然停止边界上的一次 awaited 通知。需要再执行一步的监听器调用 `agent.steer()`,传入来源显式的 steering(中途引导)内容供模型使用;循环随后重新读取 outbox,继续执行或关闭轮次。
@@ -35,7 +35,7 @@ harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那
### 三个承重的循环决策
1. **在每个拟议步骤运行 pre-step 策略。** 循环会在首次领取和决策之前打开轮次,因此 reject 会关闭一个持久、blocked 且不含步骤或模型可见消息的轮次。即使工具续步没有新取得所有权的输入,也会提交空批次,使逐请求上下文生产方可以把会记录到日志中的消息加入这一次请求。enter 时,循环先开启步骤,再把返回批次作为 `user/message` 追加,然后派生请求。依照[一次 send 对应一个轮次的简化](../simplification/2026-07-17-one-send-one-turn.md),每个已领取 follow-up 仍是其轮次中唯一的直接提示词。
1. **在每个拟议步骤运行 pre-step 策略。** 循环会在首次领取和决策之前打开轮次,因此 reject 会关闭一个持久、blocked 且不含步骤或模型可见消息的轮次。即使工具续步没有新取得所有权的输入,也会提交空批次,使逐请求上下文生产方可以把带日志的消息加入这一次请求。enter 时,循环先开启步骤,再把返回批次作为 `user/message` 追加,然后派生请求。依照[一次 send 对应一个轮次的简化](../simplification/2026-07-17-one-send-one-turn.md),每个已领取 follow-up 仍是其轮次中唯一的直接提示词。
2. **工具执行后的 `additionalContexts` 与异步注入进入活跃批次 FIFO,并在该批次结算时追加。** `content`/`feedback` 塑造 `execute()` 返回的结果,但每项上下文都是一条独立的带来源 `user/message`,而单个步骤或组合工具可以产生许多上下文。立即追加上下文会产生 `result(c1) → context → result(c2)` 的交错,或把嵌套上下文放在外层结果之前,破坏工具调用/工具结果邻接性。因此 `ToolRunContext.deferContext()` 会在失败路径上也收集嵌套调度上下文,`execute()` 在 `ToolExecutionResult` 上暴露有序数组,循环再把它接纳到与执行期间 `agent.inject()` 调用相同的 FIFO 中。FIFO 在批次结算时,在所有已记录结果之后追加,其中也包括被中断轮次关闭之前。被接受的外层调用将 deferred contexts 保留在 decision contexts 之前;被外层阻止时则丢弃 deferred contexts,只暴露阻止 decision 显式提供的上下文。
@@ -47,13 +47,13 @@ harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那
### 边界
seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);那些属于 `dsh-hook-protocol`,因为原生插件使用类型化 decision 而无需外部钩子日志。原生插件集成测试(`packages/core/agent-loop/tests/interception.spec.ts`)通过真实循环组合这些 seam,不涉及 `hook/*` 协议。压缩(compaction)(`PreCompact`/`PostCompact`)、Notification 和 Codex `PermissionRequest` 不在本决策范围内。[审批 seam](2026-07-06-approval-seam.md) 通过 `ctx.approval` 解析 `ask` decision;终结性的单调停止由工具结果数据表达,而 `agent/turn-stopping` 是引导再执行一步的最后机会。
Service Definition 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);那些属于 `dsh-hook-protocol`,因为原生插件使用类型化 decision 而无需外部钩子日志。原生插件集成测试(`packages/core/agent-loop/tests/interception.spec.ts`)通过真实循环组合这些扩展点,不涉及 `hook/*` 协议。压缩(compaction)(`PreCompact`/`PostCompact`)、Notification 和 Codex `PermissionRequest` 不在本决策范围内。[审批 seam](2026-07-06-approval-seam.md) 通过 `ctx.approval` 解析 `ask` decision;终结性的单调停止由工具结果数据表达,而 `agent/turn-stopping` 是引导再执行一步的最后机会。
## 曾考虑的替代方案
- **将工具执行前输入重写作为本 seam 集的一部分发布**:推迟,视为越界信号;上文已阐述一致性问题(审计、历史和展示都读取执行前记录的 `tool/call.arguments`),[工具执行前输入重写提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)负责该设计。
- **将持久的 `hook/*` SessionEvents 与 seam 一起声明**:否决。原生插件使用类型化 Decision 而完全不需要钩子日志(实际示例已证明),因此持久日志属于[钩子协议库](2026-06-30-hook-protocol-lib.md),而非 seam 表面。
- **将工具执行前输入重写作为本扩展点集合的一部分发布**:推迟,视为越界信号;上文已阐述一致性问题(审计、历史和展示都读取执行前记录的 `tool/call.arguments`),[工具执行前输入重写提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)负责该设计。
- **将持久的 `hook/*` SessionEvents 与扩展点一起声明**:否决。原生插件使用类型化 Decision 而完全不需要钩子日志(实际示例已证明),因此持久日志属于[钩子协议库](2026-06-30-hook-protocol-lib.md),而非扩展表面。
## 后果
规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层做包装,终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、pre-step 领取结算、工具执行后上下文缓冲和 stopping;`dsh-tools` 负责身份封存与五阶段执行流水线。它们的约定记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/subsystems/core.md#interception-decisions) 与[工具结构](../../../../docs/subsystems/tools.md)中。ACP(Agent Client Protocol)桥接会把 blocked 无步骤轮次中的首次 pre-step reject 结算为 `end_turn`,而钩子驱动的快照端到端验证可观测的桥接行为。
规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层做包装,终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、pre-step 领取结算、工具执行后上下文缓冲和 stopping;`dsh-tools` 负责身份封存与五阶段执行流水线。它们的约定记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/subsystems/core.md#interception-decisions) 与[工具结构](../../../../docs/subsystems/tools.md)中。ACP 桥接会把 blocked 无步骤轮次中的首次 pre-step reject 结算为 `end_turn`,而钩子驱动的快照端到端验证可观测的桥接行为。

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 .agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md
2026-07-05-dynamic-workflows.md: b0fb2349e64093beab8220ea75715b55903772e3
2026-07-05-dynamic-workflows.zh.md: b66e767090e29f7e0cceb35b59eedb2ea60d12eb
2026-07-05-dynamic-workflows.md: 4dc8744f7505aeb84a14dbf5ad0cc8cb5783d234
2026-07-05-dynamic-workflows.zh.md: 5d3f5b014ff4532050bb0afd5464f7ec5e91a323

View File

@@ -10,7 +10,7 @@ The harness can delegate ONE task to ONE child (`dsh-tool-subagent`), but work t
## Decision
A workflow capability family at `packages/workflow/` in the bash seam shape (interface / implementation / consumer), plus the structured-output foundation it needs on the subagent seam.
A workflow capability family at `packages/workflow/` in the bash seam shape (Service Definition / Service provider / Consumer), plus the structured-output foundation it needs on the subagent seam.
### The script contract (Claude Code-compatible)
@@ -36,7 +36,7 @@ The engine exposes an in-process `MessageChannel` test path because main-process
**Value boundary**: `materializeFromRealm` copies outbound values and rejects functions, symbols, nested `undefined`, exotic prototypes, cycles, sparse arrays, and non-finite numbers. Data-property copies make `"__proto__"` safe; getters are read normally and a throwing getter fails loudly. `args` crosses through `workerData` and is cloned again before exposure. Realm functions are invoked rather than copied, and thrown values use a total renderer so `result` cannot reject. Hook errors are host-realm `WorkflowError`s, so scripts branch on `name` or `code` rather than `instanceof Error`, as documented in the engine README. Concurrency, total-agent, item, timeout, and grace limits are validated config.
### The consumer (dsh-tool-workflow)
### The Consumer (`dsh-tool-workflow`)
A `workflow` tool mirroring `dsh-tool-subagent`'s synchronous shape: start, await, `try/finally` dispose, abort-bridge `exec.signal`, non-`completed` → `isError`. Render intent: a `generic` card titled by the call's `meta.name` parameter (presentation is a pure function of args). The tool description IS the model-facing authoring spec. The usage policy ships with the tool as its own `tool:<toolName>` prompt section (explicit-ask-only guidance — tool guidance lives in tool plugins, never in the deployment persona); the harness has no ultracode-style effort gate.

View File

@@ -10,7 +10,7 @@ harness 可以将一个任务委派给一个子 agent(`dsh-tool-subagent`)
## 决策
在 `packages/workflow/` 下以 bash seam 的形态(接口/实现/消费方)提供一组工作流能力,以及它在 subagent seam 上所需的结构化输出基础。
在 `packages/workflow/` 下以 bash seam 的形态(Service Definition/Service provider/Consumer)提供一组工作流能力,以及它在 subagent seam 上所需的结构化输出基础。
### 脚本约定(兼容 Claude Code)
@@ -36,7 +36,7 @@ harness 可以将一个任务委派给一个子 agent(`dsh-tool-subagent`)
**值边界**:`materializeFromRealm` 复制出站值,并拒绝函数、symbol、嵌套 `undefined`、异域原型、循环引用、稀疏数组和非有限数字。数据属性复制使 `"__proto__"` 安全;getter 正常读取,抛出异常的 getter 会明确报错。`args` 通过 `workerData` 传入,暴露前再次克隆。realm 函数被调用而非复制,抛出的值使用对所有输入均有定义的渲染器,因此 `result` 不会 reject。钩子错误是宿主 realm 的 `WorkflowError`,脚本应基于 `name` 或 `code` 分支而非 `instanceof Error`,如引擎 README 所述。并发、total-agent、item、超时和宽限限制均为经校验的配置。
### 消费方(dsh-tool-workflow)
### Consumer(`dsh-tool-workflow`)
一个 `workflow` 工具,镜像 `dsh-tool-subagent` 的同步形态:启动、await、`try/finally` dispose、abort 桥接 `exec.signal`、非 `completed` → `isError`。渲染意图:一张以调用的 `meta.name` 参数为标题的 `generic` 卡片(展示是参数的纯函数)。工具描述即面向模型的编写规范。使用策略以工具自身的 `tool:<toolName>` 提示词段落随工具发布(显式请求才使用的引导——工具引导存在于工具插件中,从不在部署 persona 中);harness 没有 ultracode 风格的 effort 门控。

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 .agents/notes/implemented/feature/2026-07-05-skill-system.md
2026-07-05-skill-system.md: 5961c535a1397c487b0255f8a27d60929c2eeace
2026-07-05-skill-system.zh.md: 42ae08e603411bee6212f6f321eb766c9b9d1859
2026-07-05-skill-system.md: c4b177d84c97e5c541bffb6dfeb2135aedb8d5b8
2026-07-05-skill-system.zh.md: d0ca688e96c10e674c9b92da8805d653ce5fd063

View File

@@ -24,7 +24,7 @@ Each skill is either `<name>/SKILL.md` or `<name>.md` with YAML frontmatter. `na
Local skill filesystem I/O goes through `ctx.fs` when a filesystem service is loaded: project-root lookup probes `.git` with `resolve` and `stat`, root discovery uses `listDir`, and skill reads use `readText`. The Node filesystem remains a fallback for minimal contexts that mount `dsh-skill-local` without the fs seam. Missing roots, unreadable or malformed skill files, and transient provider `list()` failures degrade to warn-and-skip so one bad source does not make every agent request fail; malformed candidates still fail fast because they are provider contract violations.
`dsh-tool-skill` injects one durable user-role `<system-reminder>` catalog as a sourced `user/message` at the session's first `agent/pre-step`, and only when that agent's tool view resolves this plugin's exact `skill` registration. The catalog contains sorted skill name and description only; it excludes bodies, paths, sources, providers, and routing hints. Descriptions are whitespace-normalized, XML-escaped, and capped by `catalogDescriptionMaxLength`, whose default is `500` and minimum is `3`. Full skill bodies are never included in the catalog. (The catalog originally rode the request-only [session-prefix seam](../../archived/feature/2026-07-07-session-prefix.md), archived; the [unified sourced-message decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md) moved it into durable history.)
`dsh-tool-skill` injects one durable user-role `<system-reminder>` catalog as a sourced `user/message` at the session's first `agent/pre-step`, and only when that agent's tool view resolves this plugin's exact `skill` registration. The catalog contains sorted skill name and description only; it excludes bodies, paths, sources, providers, and routing hints. Descriptions are whitespace-normalized, XML-escaped, and capped by `catalogDescriptionMaxLength`, whose default is `500` and minimum is `3`. Full skill bodies are never included in the catalog. (The catalog originally rode the request-only [session-prefix extension point](../../archived/feature/2026-07-07-session-prefix.md), archived; the [unified sourced-message decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md) moved it into durable history.)
The registry's `list()` returns every winning summary, while model and user consumers apply the invocation predicates owned by the [independent invocation-policy decision](2026-07-28-skill-invocation-policy.md). The `skill({ name })` tool loads one model-invocable skill for the current agent cwd and returns a tool result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`. `resourceBase` supplies a directory, URL, or opaque provider-managed base for explicitly referenced scripts, references, and assets; resources load only as needed, without directory enumeration. An unresolved name reports that the skill is unknown or no longer available; invalid names and skills with `invocation.modelInvocable: false` retain distinct tool errors. The tool result is the model-visible disclosure path.
@@ -38,7 +38,7 @@ The data structures and catalog/tool contract are documented in [skills.md](../.
**Put local filesystem scanning directly inside `ctx.skills`.** Rejected because coding agents, web agents, and future plugin ecosystems need different skill sources. A provider registry mirrors the subagent seam: the registry owns conflict resolution and consumers, while implementations own loading.
**Use a system-prompt section.** Rejected because the rendered system prompt is a single string, while the catalog is a user-role `<system-reminder>` message. The [request-only session-prefix seam](../../archived/feature/2026-07-07-session-prefix.md) (archived) was the original mechanism; after the unified sourced-message decision removed that seam, the catalog became a durable sourced injection with the same message shape.
**Use a system-prompt section.** Rejected because the rendered system prompt is a single string, while the catalog is a user-role `<system-reminder>` message. The [request-only session-prefix extension point](../../archived/feature/2026-07-07-session-prefix.md) (archived) was the original mechanism; after the unified sourced-message decision removed it, the catalog became a durable sourced injection with the same message shape.
**Materialize built-in DSH authoring skills under `~/.dsh/skills/.system`.** Rejected because bundled skills do not write user home on startup, and embedded or remote providers supply configured skills.

View File

@@ -24,7 +24,7 @@ DeepSeek Harness 使用同一原语,使项目特定的评审、插件编写和
本地 skill 的文件系统 I/O 在加载了文件系统服务时通过 `ctx.fs` 进行:项目根目录查找使用 `resolve` 和 `stat` 探测 `.git`,根目录发现使用 `listDir`,skill 读取使用 `readText`。Node 文件系统作为后备,供在不挂载 fs seam 的最小上下文中加载 `dsh-skill-local` 时使用。缺失的根目录、不可读或格式错误的 skill 文件、以及提供方 `list()` 的瞬态失败均降级为警告并跳过,使一个坏源不会导致所有 agent 请求失败;格式错误的候选项仍然快速失败,因为它们违反了提供方约定。
`dsh-tool-skill` 在会话的第一个 `agent/pre-step` 注入一个持久化的 user-role `<system-reminder>` 目录,作为带来源的 `user/message`,且仅当该 agent 的工具视图解析到本插件精确的 `skill` 注册时才注入。该目录仅包含排序后的 skill 名称与描述;不包含正文、路径、来源、提供方和路由提示。描述经过空白规范化、XML 转义,并受 `catalogDescriptionMaxLength` 上限约束,其默认值为 `500`,最小值为 `3`。完整的 skill 正文从不包含在目录中。(目录最初通过仅请求的[会话前缀 seam](../../archived/feature/2026-07-07-session-prefix.md)(已归档)传递;[统一带来源消息的决策](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)将其移入持久化历史。)
`dsh-tool-skill` 在会话的第一个 `agent/pre-step` 注入一个持久化的 user-role `<system-reminder>` 目录,作为带来源的 `user/message`,且仅当该 agent 的工具视图解析到本插件精确的 `skill` 注册时才注入。该目录仅包含排序后的 skill 名称与描述;不包含正文、路径、来源、提供方和路由提示。描述经过空白规范化、XML 转义,并受 `catalogDescriptionMaxLength` 上限约束,其默认值为 `500`,最小值为 `3`。完整的 skill 正文从不包含在目录中。(目录最初通过仅请求的[会话前缀扩展点](../../archived/feature/2026-07-07-session-prefix.md)(已归档)传递;[统一带来源消息的决策](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)将其移入持久化历史。)
注册表的 `list()` 返回全部胜出摘要,而模型与用户消费方应用[独立调用策略决策](2026-07-28-skill-invocation-policy.md)定义的调用判定。`skill({ name })` 工具为当前 agent cwd 加载一个模型可调用的 skill,返回包含 `<skill_content name="...">`、`<skill_resources>` 和 `<skill_instructions>` 的工具结果。`resourceBase` 提供一个目录、URL 或不透明的提供方管理的基路径,用于显式引用的脚本、参考资料和资产;资源仅按需加载,不进行目录枚举。无法解析的名称报告该 skill 未知或不再可用;无效名称和 `invocation.modelInvocable` 为 `false` 的 skill 保留不同的工具错误。工具结果是面向模型的可见披露路径。
@@ -38,7 +38,7 @@ DeepSeek Harness 使用同一原语,使项目特定的评审、插件编写和
**将本地文件系统扫描直接放入 `ctx.skills`。** 否决,因为编码 agent、Web agent 和未来的插件生态需要不同的 skill 来源。提供方注册表与 subagent seam 镜像:注册表拥有冲突解决和消费方,实现拥有加载。
**使用系统提示词段落。** 否决,因为渲染后的系统提示词是单一字符串,而目录是一条 user-role `<system-reminder>` 消息。[仅请求的会话前缀 seam](../../archived/feature/2026-07-07-session-prefix.md)(已归档)是最初的机制;统一带来源消息的决策移除该 seam 后,目录改为具有相同消息形状的持久化带来源注入。
**使用系统提示词段落。** 否决,因为渲染后的系统提示词是单一字符串,而目录是一条 user-role `<system-reminder>` 消息。[仅请求的会话前缀扩展点](../../archived/feature/2026-07-07-session-prefix.md)(已归档)是最初的机制;统一带来源消息的决策移除该扩展点后,目录改为具有相同消息形状的持久化带来源注入。
**在 `~/.dsh/skills/.system` 下物化内置 DSH 编写 skill。** 否决,因为打包的 skill 不应在启动时写入用户主目录,嵌入式或远程提供方在配置后提供 skill。

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 .agents/notes/implemented/feature/2026-07-06-approval-seam.md
2026-07-06-approval-seam.md: a286e07d51a70232f9aab0af5d8a19622cd222d4
2026-07-06-approval-seam.zh.md: b15fcb2f6ed52ee591082770d68e265b5a703ff8
2026-07-06-approval-seam.md: 7c830d93f19a40ab193cfebabca854882ab68d62
2026-07-06-approval-seam.zh.md: 9dedfddadc23b0da44b28e8750508653ee20bb83

View File

@@ -97,7 +97,7 @@ Snapshots record allowed and rejected sandbox escalation through `session/reques
- **An inline `tools/pre-execute` permission gate in the ACP bridge** — rejected: prompting for every bridge-owned call hardwires the asking policy into the transport, cannot serve a second asker (sandbox escalation happens after execution starts, with no pre-execute moment), and leaves hook-produced `ask` decisions without a shared mechanism.
- **The generic user-interaction seam (`ctx.userInteraction`)** — rejected as the approval mechanism: the two share a skeleton (route by agent, block for a human, handle absence), but approval's contract is narrower in every dimension that matters: a closed outcome vocabulary instead of free text, a protocol-native prompt attached to a tool call instead of a generic form, mandatory fail-closed absence, and audit events. Approval therefore does not ride the shipped `packages/interaction/user-interaction` / `ask_user_question` elicitation path — an elicitation form is not a permission prompt, and a free-text answer is not a closed outcome; sharing provider plumbing stays open if the two ever converge.
- **Static optional injection in `dsh-tools`** — rejected: the vendored cordis `Inject` type has no optional flag — the object form maps service names to intercept config, and a declared inject gates the fiber. `ctx.get('approval')` is the documented opportunistic-consumption pattern (the `tool-bash` owner-token lookup, the loop's persistence probe), reads presence per call, and degrades correctly across HMR without extra machinery.
- **The capability-seam three-package split** — rejected: interface/implementation/consumer fits a seam whose implementation is swappable (bash-local vs bash-sandbox). Here the service body is fixed mechanism and the variable part is listeners that live with their owners — splitting would manufacture an implementation package with nothing in it ("don't split preemptively").
- **The capability-seam three-package split** — rejected: Service Definition / Service provider / Consumer fits a seam whose Service provider is swappable (bash-local vs bash-sandbox). Here the service body is fixed mechanism and the variable part is listeners that live with their owners — splitting would manufacture a Service provider package with nothing in it ("don't split preemptively").
- **Offering `allow_always` now** — rejected: the protocol can express it, but honoring it means designing grant storage, scope identity, and revocation (§ Deferred). Advertising an option the harness cannot honor manufactures doomed grants.
## Consequences
@@ -134,6 +134,6 @@ In-repo precedents this design copies or contrasts with:
- The `fs/write-intent` gate (`packages/fs/fs/`) — the documented single-occupancy decision-slot waterfall semantics (first answer wins, delegate via `next()`) the answerer contract reuses.
- `hook/invoked`/`hook/result` — the log-only audit-pair precedent `approval/asked`/`approval/decided` follows; [the hook-bridges Agent Note](2026-06-30-hook-bridges.md) ships `permissionDecision: ask`, the first producer.
- [The interception-seams Agent Note](2026-06-30-interception-seams.md) — the `tools/pre-execute` `allow`/`deny`/`ask` vocabulary whose `ask` this seam services.
- [The interception extension-points Agent Note](2026-06-30-interception-extension-points.md) — the `tools/pre-execute` `allow`/`deny`/`ask` vocabulary whose `ask` this seam services.
- [The automation-only ACP Agent Note](../simplification/2026-07-23-acp-automation-only-protocol.md) — the exact-agent ownership check against the session map that the answerer routes through; [the multi-session Agent Note](2026-06-14-acp-multi-session.md) — the per-session permission-ownership blocker this implements.
- The opportunistic `ctx.get()` consumption pattern (`tool-bash`'s owner-token lookup, the loop's persistence probe) — how `dsh-tools` consumes the seam without gating its fiber on it.

View File

@@ -97,7 +97,7 @@ ACP 桥只应答其会话映射所拥有的精确 agent 对象。它携带既有
- **在 ACP 桥中内联 `tools/pre-execute` 权限门禁**:否决。对桥拥有的每次调用都弹出提示,会将请求策略硬编码进传输层,无法服务第二个发起方(沙箱升级发生在执行开始之后,没有 pre-execute 时刻),且钩子产生的 `ask` 决策没有共享机制。
- **通用用户交互 seam(`ctx.userInteraction`)**:否决作为审批机制。二者骨架相似(按 agent 路由、阻塞等待人类、处理缺失),但审批的约定在每个关键维度上都更窄:封闭的结果词汇而非自由文本、附着在工具调用上的协议原生提示而非通用表单、强制的缺失时失败关闭、以及审计事件。因此审批不走已交付的 `packages/interaction/user-interaction` / `ask_user_question` 信息征集路径——信息征集表单不是权限提示,自由文本应答不是封闭结果;如果二者将来趋同,共享提供方管道仍然开放。
- **`dsh-tools` 中的静态可选注入**:否决。vendor 的 Cordis `Inject` 类型没有 optional 标志——对象形式将服务名映射到拦截配置,声明的 inject 会阻塞 fiber。`ctx.get('approval')` 是文档化的机会性消费模式(`tool-bash` 的 owner-token 查找、loop 的持久化探测),按调用读取存在性,跨 HMR 正确降级,无需额外机制。
- **能力 seam 的三包拆分**:否决。接口/实现/消费方适合实现可替换的 seam(bash-local vs bash-sandbox)。此处服务体是固定机制,可变部分是留在各自通道拥有者插件中的监听器——拆分只会制造一个空的实现包(「不要预防性拆分」)。
- **能力 seam 的三包拆分**:否决。Service Definition/Service provider/Consumer 适合 Service provider 可替换的 seam(bash-local vs bash-sandbox)。此处服务体是固定机制,可变部分是留在各自通道拥有者插件中的监听器——拆分只会制造一个空的 Service provider 包(「不要预防性拆分」)。
- **现在就提供 `allow_always`**:否决。协议能表达它,但兑现它意味着设计授权存储、作用域标识和撤销(§ 延后)。展示 harness 无法兑现的选项只会制造注定失败的授权。
## 后果
@@ -134,6 +134,6 @@ ACP 桥只应答其会话映射所拥有的精确 agent 对象。它携带既有
- `fs/write-intent` 门禁(`packages/fs/fs/`)——文档化的单占用决策槽 waterfall 语义(先到先得,通过 `next()` 委派),应答者约定复用了它。
- `hook/invoked`/`hook/result`——仅日志审计对先例,`approval/asked`/`approval/decided` 沿用了它;[钩子桥 Agent Note](2026-06-30-hook-bridges.md) 交付了 `permissionDecision: ask`,即第一个生产者。
- [拦截 seam Agent Note](2026-06-30-interception-seams.md)——`tools/pre-execute` 的 `allow`/`deny`/`ask` 词汇,本 seam 服务其中的 `ask`。
- [拦截扩展点 Agent Note](2026-06-30-interception-extension-points.md)——`tools/pre-execute` 的 `allow`/`deny`/`ask` 词汇,本 seam 服务其中的 `ask`。
- [仅面向自动化的 ACP Agent Note](../simplification/2026-07-23-acp-automation-only-protocol.md)——应答者路由时对会话映射执行的精确 agent 归属检查;[多会话 Agent Note](2026-06-14-acp-multi-session.md)——本设计实现的每会话权限归属阻塞项。
- 机会性 `ctx.get()` 消费模式(`tool-bash` 的 owner-token 查找、loop 的持久化探测)——`dsh-tools` 消费该 seam 而不阻塞其 fiber 的方式。

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 .agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md
2026-07-06-explicit-tool-order.md: bd6d0a04aa470ca33e618957ae1f08c1ef15fcfe
2026-07-06-explicit-tool-order.zh.md: 69365151f2c20cfa782ecc5560a6c600c8dec6a2
2026-07-06-explicit-tool-order.md: 7a4553b50da409b531a9fe14fcfbb201a5165fc4
2026-07-06-explicit-tool-order.zh.md: a78a38a6ada3ed0a2f3a756830747916446e188c

View File

@@ -39,7 +39,7 @@ Config plumbing follows the `persona` precedent, and `toolOrder` sits beside it:
## Consequences
- Every registry-built assembly starts with a deterministic tool order on every host; absent an expert listener that deliberately changes it, every `request/header` event and model request inherits that order. The CI-vs-local registration-order flip is structurally gone, and the default is lexicographic.
- The initial `PromptAssembly.tools` is canonical, so waterfall listeners start from the model-facing order; provider registration order is observable nowhere before that cooperative seam.
- The initial `PromptAssembly.tools` is canonical, so waterfall listeners start from the model-facing order; provider registration order is observable nowhere before that cooperative extension point.
- The snapshot suite's single pinned request-header fixture (`text-turn`) carries the new canonical tool order; every other ACP snapshot keeps the header bulk scrubbed as `{{system}}`/`{{tools}}`, per the pinned-header design.
- A pure tool reordering between steps is logged like any other header change: a full `request/header` snapshot with reason `'change'`. Stable canonical order prevents registration timing from creating such changes in the ordinary path.
- The `toolOrder` key rides the app → `agent-core` → `SystemPrompt` forwarding chain, so deployments set it next to `persona` in the app config; `dsh-llm` and the agent loop are untouched.

View File

@@ -39,7 +39,7 @@ Status: implemented
## 后果
- 每个由注册表构建的组装在任何宿主上都以确定性工具顺序开始;在没有专家监听器刻意改变的情况下,每个 `request/header` 事件和模型请求都继承该顺序。CI 与本地之间的注册顺序翻转从结构上被消除,默认为字典序。
- 初始 `PromptAssembly.tools` 是权威的,因此 waterfall 监听器从模型侧顺序开始;提供方注册顺序在该协作 seam 之前无处可观测。
- 初始 `PromptAssembly.tools` 是权威的,因此 waterfall 监听器从模型侧顺序开始;提供方注册顺序在该协作扩展点之前无处可观测。
- 快照套件中唯一锁定请求头的 fixture(`text-turn`)携带新的权威工具顺序;按照锁定请求头的设计,其他 ACP 快照仍将大段 header 替换为 `{{system}}`/`{{tools}}`。
- 步骤之间的纯工具重排与其他 header 变更一样记录:一份原因是 `'change'` 的完整 `request/header` 快照。稳定的权威顺序会防止注册时序在普通路径上制造这类变化。
- `toolOrder` 键沿 app → `agent-core` → `SystemPrompt` 的转发链传递,因此部署时将其放在 app 配置中 `persona` 旁边即可;`dsh-llm` 和 agent loop 无需改动。

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 .agents/notes/implemented/feature/2026-07-06-sandbox.md
2026-07-06-sandbox.md: 06c5590454a6947030d828f087b92f44207dad6e
2026-07-06-sandbox.zh.md: 85b9b6a65b6468b19c6044b4837abc4fab1cee1a
2026-07-06-sandbox.md: 0214a202a41983c76fa25e3a82e1cfaec70a0d55
2026-07-06-sandbox.zh.md: bd8dcdb74f723a955ea2a1cc5b224ef2ded4a8d5

View File

@@ -38,7 +38,7 @@ Four `cordis.yml` entries turn an unconfined coding agent into the sandboxed pro
The swap is invisible to every consumer of `ctx.bash`: the bash tools, hook commands, and background tasks run exactly as before by directly spawning the wrapped argv the provider returns. Deleting the `sandbox` and `permission` entries and replacing `bash` with `@deepseek-ai/dsh-bash-local` is the opt-out — execution is unconfined again and the escalation fields vanish from the tool schema, because they are capability-gated on the mounted executor, not on configuration. Omitting only `approval` keeps confinement but fails every escalation closed with its own error text; `permission` also requires the approval seam and a confining executor, so a partially composed preset layer fails loud at load.
Misconfiguration fails loud: `mode` outside the closed vocabulary is rejected at plugin load, and a host with no usable backend throws the structured `SANDBOX_UNAVAILABLE` at `confine()` rather than degrading to unconfined execution. If the selected runner rejects with attributable `ENOENT` or `EACCES`, the consumer reports the same infrastructure error from the spawn channel before any command starts; other spawn errors retain local command-start semantics while still running nothing. `runnerCommand` on `dsh-sandbox-local` is the operator's explicit assertion of a bwrap-compatible runner (chain and probes skipped); it doubles as the deterministic fake-runner seam for keyless tests.
Misconfiguration fails loud: `mode` outside the closed vocabulary is rejected at plugin load, and a host with no usable backend throws the structured `SANDBOX_UNAVAILABLE` at `confine()` rather than degrading to unconfined execution. If the selected runner rejects with attributable `ENOENT` or `EACCES`, the consumer reports the same infrastructure error from the spawn channel before any command starts; other spawn errors retain local command-start semantics while still running nothing. `runnerCommand` on `dsh-sandbox-local` is the operator's explicit assertion of a bwrap-compatible runner (chain and probes skipped); it doubles as the deterministic fake-runner hook for keyless tests.
Denied file effects return a `[sandbox: file access denied under <mode> mode]` marker and instructions not to work around the denial. A confining executor adds paired `sandbox_permissions` and `justification` fields for one approved retry that must be strictly wider than the session's effective mode. A grant widens only that retry; rejection executes nothing, returns `the user rejected escalating this command to "<mode>"`, and permits no re-ask. The owner-derived pending policy context states the current file policy without replacing those enforcement boundaries. When `dsh-permission` is composed with a UI adapter, one preset selects both knob values; unmatched values fold to `custom`. The [ACP automation composition](../../../../examples/acp-agent/README.md) does not mount that UI service and selects its deployment mode explicitly.
@@ -54,7 +54,7 @@ OS subprocess confinement applies to the bash executor, including hook commands,
Policy rides each CALL, not the provider: two consumers may confine under different policies at the same instant (bash under `read-only` while a confined child agent keeps its state directory writable), and an approved escalated retry is a new call with a wider policy — inexpressible under a config-fixed provider mode.
The seam confines SAME-WORLD subprocesses only: a backend shares the host's filesystem and kernel. Containers, microVMs, and remote executors are NOT backends of this seam — they replace whole capability implementations (`ctx.bash`, `ctx.fs`) as environment-coherent groups, because an agent whose bash runs in a container while its fs tools write the host lives in two split worlds.
The seam confines SAME-WORLD subprocesses only: a backend shares the host's filesystem and kernel. Containers, microVMs, and remote executors are NOT backends of this seam — they replace the Service providers for whole capability seams (`ctx.bash`, `ctx.fs`) as environment-coherent groups, because an agent whose bash runs in a container while its fs tools write the host lives in two split worlds.
Left open, for the phase that needs them: whether network restriction arrives as a separate `network_mode` or merges into `sandbox_mode` once a runner enforces both, and whether `SandboxPolicy` grows extra writable-root grants now (the launcher already speaks `--rw <path>`) or only when escalation needs them.
@@ -70,7 +70,7 @@ Backend profiles share the mode contract but differ in necessary host grants. La
#### The bash consumer
`dsh-bash-sandbox` extends `LocalBashExecutor`, hands `ctx.sandbox` the exact `['bash', '-c', command]` argv, and directly spawns the provider result. This leaves shell semantics and `BASH_ENV` on the inner Bash after the shipped native runner establishes confinement. A provider error propagates unchanged. A pre-process rejection is runner-owned only when the caller-owned workdir is independently usable and Node reports `ENOENT` or `EACCES` with positive provenance for provider argv[0]; a bare `syscall: 'spawn'` without an exact error path, other codes, invalid workdirs, resource failures, unrelated syscalls, and unstructured rejections retain local command-start semantics. Foreground execution converts runner-owned rejections to `SANDBOX_UNAVAILABLE` with the original detail; an asynchronous background rejection stamps `runnerFailed: true`, `denied: false`. A `SubprocessService` that synchronously throws the same provenanced shape makes background start throw `SANDBOX_UNAVAILABLE`, while other synchronous errors propagate unchanged. After a process starts, foreground and background use one runner-failure classifier that requires the rule's exit-code gate and a remaining fatal line after informational exclusions. A match outranks denial: foreground execution throws `SANDBOX_UNAVAILABLE` with that fatal line as detail; a settled `BashProcess` stamps `sandbox.runnerFailed`, and the bash producer renders it through generic `task_output`.
`dsh-bash-sandbox` extends `LocalBashExecutor`, hands `ctx.sandbox` the exact `['bash', '-c', command]` argv, and directly spawns the provider result. This leaves shell semantics and `BASH_ENV` on the inner Bash after the shipped native runner establishes confinement. A provider error propagates unchanged. A pre-process rejection counts as a runner failure only when the caller-owned workdir is independently usable and Node reports `ENOENT` or `EACCES` with either an `error.path` equal to provider argv[0] or, when `error.path` is absent, an exact `syscall: 'spawn <runner>'`; a present path also requires `syscall: 'spawn'` or the exact `spawn <runner>`. Other codes, invalid workdirs, resource failures, unrelated syscalls, and unstructured rejections retain local command-start semantics. Foreground execution converts runner failures to `SANDBOX_UNAVAILABLE` with the original detail; an asynchronous background rejection stamps `runnerFailed: true`, `denied: false`. A `SubprocessService` that synchronously throws the same runner-identifying shape makes background start throw `SANDBOX_UNAVAILABLE`, while other synchronous errors propagate unchanged. After a process starts, foreground and background use one runner-failure classifier that requires the rule's exit-code check and a remaining fatal line after informational exclusions. A match takes priority over denial: foreground execution throws `SANDBOX_UNAVAILABLE` with that fatal line as detail; a settled `BashProcess` stamps `sandbox.runnerFailed`, and the bash producer renders it through generic `task_output`.
The model sees the current effective file policy in the owner-derived `sandbox:policy` context, while the static tool description explains the denial marker (`[sandbox: file access denied under <mode> mode]`), encourages attempting commands that may be denied, and forbids retrying around a denial; when the escalation fields are advertised, a denied result additionally carries the escalation hint itself, so the sanctioned same-turn retry is prompted at the decision point rather than depending on the model recalling the description (§ Escalation). [The current-policy decision](2026-07-30-current-sandbox-policy-context.md) owns the context's rationale and boundaries.
@@ -82,7 +82,7 @@ The model sees the current effective file policy in the owner-derived `sandbox:p
When a confining executor is mounted, `bash` advertises paired `sandbox_permissions` and `justification` fields. The schema exposes the full closed escalation vocabulary because effective mode is per-session; execution rejects any target that is not strictly wider than that call's effective mode. Approval resolves before execution. `allowed-once` stamps the granted mode onto only that request, while `rejected`, `cancelled`, `unavailable`, a missing approval service, or a missing agent all fail closed with distinct results. No grant is persisted.
Escalation is a same-turn retry of the denied command with the narrowest sufficient `sandbox_permissions` and a `justification`; the approval prompt is the consent step. It must be grounded in an actual denial, except when the session already observed the same denied access, and a disabled or rejected approval ends that command. The retry, approval decision, and result use existing tool and approval events. `dsh-tool-bash` owns the ask because the executor seam has neither the agent nor call id required for user interaction.
Escalation is a same-turn retry of the denied command with the narrowest sufficient `sandbox_permissions` and a `justification`; the approval prompt is the consent step. It must be grounded in an actual denial, except when the session already observed the same denied access, and a disabled or rejected approval ends that command. The retry, approval decision, and result use existing tool and approval events. `dsh-tool-bash` owns the ask because the executor Service Definition has neither the agent nor call id required for user interaction.
Left open: what a durable grant's scope identity is beyond the sandbox mode — exact call, path, command prefix, session, or time window — before an `allow_always` option can be advertised.
@@ -113,7 +113,7 @@ Before each proposed step, sandbox and approval policy are rendered as ordered c
#### In-process tools
fs/web/todo execute in-process, so their sandbox semantics are policy at their seams. The fs seam now enforces the shared mode vocabulary through a sandboxed provider (`dsh-fs-sandbox` fences write/edit by mode; see [the cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)), so `read-only`/`workspace-write` are real boundaries for the filesystem tools, not a bash-only approximation. web/todo remain unfenced (web's only effect is network, outside the file-effect mode vocabulary). No generic per-tool sandbox runtime: a host-mediated tool leaves the process only by returning declarative effects the host validates, which is a rewrite, not a wrapper — the follow-up settled on one shared policy home (`ctx.sandboxPolicy`) with per-seam enforcement, not a uniform wrapper.
fs/web/todo execute in-process, so their sandbox semantics are policy at their capability boundaries. The fs seam now enforces the shared mode vocabulary through a sandboxed provider (`dsh-fs-sandbox` fences write/edit by mode; see [the cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)), so `read-only`/`workspace-write` are real boundaries for the filesystem tools, not a bash-only approximation. web/todo remain unfenced (web's only effect is network, outside the file-effect mode vocabulary). No generic per-tool sandbox runtime: a host-mediated tool leaves the process only by returning declarative effects the host validates, which is a rewrite, not a wrapper — the follow-up settled on one shared policy home (`ctx.sandboxPolicy`) with per-capability enforcement, not a uniform wrapper.
### Testing
@@ -142,7 +142,7 @@ Each phase gets its full design when picked up, validated against the code at th
- **Config-fixed mode on the provider** — rejected: one mode per process; cannot serve concurrent consumers with different policies nor the one-shot widened retry.
- **One interface spanning containers/VMs too** — rejected: `confine(argv)` presupposes a shared filesystem; environment isolation is capability-sibling backends deployed as coherent groups.
- **Generic ToolRuntime wrapping any tool** — rejected: mechanically false for in-process tools (closures over `ctx`); the declarative-effects rewrite is unjustified for fs/web/todo.
- **Ask inside the executor (`dsh-bash-sandbox`)** — rejected: no `agent` to route through, no `callId` to attach the prompt to; adding them teaches a transport seam about sessions and UIs — the tool layer holds both and owns the model-facing vocabulary.
- **Ask inside the executor (`dsh-bash-sandbox`)** — rejected: no `agent` to route through, no `callId` to attach the prompt to; adding them teaches a transport boundary about sessions and UIs — the tool layer holds both and owns the model-facing vocabulary.
- **Auto-retry inside the same tool call** — rejected: a hidden re-entry the log cannot reconstruct: one `tool/call` would have produced two executions with different policies — the retry is a NEW logged call with its own arguments and result facts.
- **Advertise the escalation fields unconditionally** — rejected: under `dsh-bash-local` they are a dead lever — advertising an option the harness cannot honor manufactures doomed grants; capability-gating costs one registration-time read.
- **A default-relative escalation ladder (advertise only the modes wider than the executor's registration-time default)** — rejected: per-session overrides make the default the wrong baseline — a session switched narrower than the default loses exactly the lever it needs, and under a `danger-full-access` default the fields vanish entirely while a `read-only`-overridden session stays confined with no escalation path. The enum pins the closed target vocabulary; strict widening is a per-call execution check against the session's effective mode.
@@ -185,7 +185,7 @@ Costs and accepted limits:
## FAQ
- **A command came back with `[sandbox: file access denied under read-only mode]` — did it fail?** It RAN, and the kernel refused a file effect: the denial is a result fact orthogonal to exit code. The teaching forbids retrying around it; the one sanctioned move is the same command retried once with an escalation request.
- **How is a BROKEN sandbox told apart from a failing command?** Any provider-argv spawn rejection proves the confined launch never started, but it identifies a broken runner only when the caller-owned workdir is usable and Node reports attributable `ENOENT` or `EACCES` for that argv[0]. A bare `syscall: 'spawn'` without an exact error path and all other rejections remain ordinary command-start errors. After a process starts, runner failure outranks denial only when one `runnerFailureRules` entry matches both its optional exit-code gate and a fatal stderr line after exact informational exclusions. Foreground failures throw structured `SANDBOX_UNAVAILABLE` with spawn or matched-line detail; an asynchronously rejected or settled background task stamps `sandbox.runnerFailed` and renders its own marker. A `SubprocessService` that synchronously throws the same provenanced `ENOENT`/`EACCES` shape makes background start throw the structured error; other synchronous errors propagate unchanged. A Landlock partial-enforcement notice plus an ordinary child failure remains a command result.
- **How is a BROKEN sandbox told apart from a failing command?** Any provider-argv spawn rejection proves the confined launch never started, but it identifies a broken runner only when the caller-owned workdir is usable and Node reports attributable `ENOENT` or `EACCES` for that argv[0]. A bare `syscall: 'spawn'` without an exact error path and all other rejections remain ordinary command-start errors. After a process starts, runner failure outranks denial only when one `runnerFailureRules` entry matches both its optional exit-code gate and a fatal stderr line after exact informational exclusions. Foreground failures throw structured `SANDBOX_UNAVAILABLE` with spawn or matched-line detail; an asynchronously rejected or settled background task stamps `sandbox.runnerFailed` and renders its own marker. A `SubprocessService` that synchronously throws the same `ENOENT`/`EACCES` shape with the runner path makes background start throw the structured error; other synchronous errors propagate unchanged. A Landlock partial-enforcement notice plus an ordinary child failure remains a command result.
- **What happens on a platform with no backend — Windows today?** `confine()` throws the fail-closed `SANDBOX_UNAVAILABLE` and the command never spawns; `win32` is a reserved EMPTY chain, pinned by test to fail closed identically until a Windows runner fills it (§ Deferred phases).
- **`bwrap` is installed on my host but unusable (disabled unprivileged userns, an LSM denying `mount`) — what happens?** The chain probe is functional — it builds and enforces a real profile rather than checking `--version` — so a present-but-unusable `bwrap` fails its probe, selection falls to the packaged Landlock launcher, and the verdict is cached for the provider's lifetime.
- **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam.
@@ -199,8 +199,8 @@ Costs and accepted limits:
In-repo precedents this design copies or contrasts with:
- [The capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md) — the interface/implementation/consumer split and the "don't split preemptively" timing rule the second consumer satisfied.
- [The capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md) — the Service Definition / Service provider / Consumer split and the "don't split preemptively" timing rule the second consumer satisfied.
- The `dsh-bash` request/spec split ([the bash vocabulary catalog](../../../../docs/subsystems/bash.md)) — the complete `sandboxPolicy` rides its per-call carrier, and the explicit-`resolve()` defaulting convention.
- [The approval seam Agent Note](2026-07-06-approval-seam.md) — the channel escalation asks through; its answerer waterfall, audit pair, and one-package rationale are recorded there.
- [Event-sourced sessions](../architecture/2026-06-11-event-sourced-sessions.md) and [standalone log-only events](../simplification/2026-07-28-remove-synthetic-log-only-turns.md) — the log-as-store foundation the per-session modes fold over, and the explicit durability boundary the anchoring design obeys.
- [The interception-seams Agent Note](2026-06-30-interception-seams.md) — the `tools/pre-execute` vocabulary the escalation gate deliberately does not reuse (an escalating call has no pre-execute moment of its own).
- [The interception extension-points Agent Note](2026-06-30-interception-extension-points.md) — the `tools/pre-execute` vocabulary the escalation gate deliberately does not reuse (an escalating call has no pre-execute moment of its own).

View File

@@ -38,7 +38,7 @@ harness 是一个 SDK,因此约束必须是开发者可组合的能力:是
这一替换对 `ctx.bash` 的所有消费方透明:bash 工具、钩子命令和后台任务照常运行,直接使用提供方返回的已包装 argv 启动。删除 `sandbox` 和 `permission` 条目、将 `bash` 替换为 `@deepseek-ai/dsh-bash-local` 即为退出——执行恢复为无约束,升级字段从工具 schema 中消失,因为它们是基于已挂载执行器的能力门控,而非基于配置。仅省略 `approval` 则保留约束但以自身错误文本关闭每次升级;`permission` 还要求 approval seam 和约束执行器同时存在,因此部分组合的 preset 层在加载时即大声失败。
配置错误会显式导致失败:`mode` 不在封闭词汇中时在插件加载时被拒绝;主机上没有可用后端时在 `confine()` 阶段抛出结构化的 `SANDBOX_UNAVAILABLE`,而非降级为无约束执行。如果所选 runner 以可归因的 `ENOENT` 或 `EACCES` 拒绝,消费方会在任何命令开始前通过 spawn 通道报告同一基础设施错误;其他 spawn 错误仍保留本地命令启动语义,同时也不会运行任何内容。`dsh-sandbox-local` 上的 `runnerCommand` 是运维人员对一个 bwrap 兼容 runner 的显式断言(跳过链和探测);它同时充当 keyless 测试的确定性 fake-runner seam。
配置错误会显式导致失败:`mode` 不在封闭词汇中时在插件加载时被拒绝;主机上没有可用后端时在 `confine()` 阶段抛出结构化的 `SANDBOX_UNAVAILABLE`,而非降级为无约束执行。如果所选 runner 以可归因的 `ENOENT` 或 `EACCES` 拒绝,消费方会在任何命令开始前通过 spawn 通道报告同一基础设施错误;其他 spawn 错误仍保留本地命令启动语义,同时也不会运行任何内容。`dsh-sandbox-local` 上的 `runnerCommand` 是运维人员对一个 bwrap 兼容 runner 的显式断言(跳过链和探测);它同时充当 keyless 测试的确定性 fake-runner 钩子。
被拒绝的文件操作返回 `[sandbox: file access denied under <mode> mode]` 标记,并附带不要绕过拒绝的指令。约束执行器添加配对的 `sandbox_permissions` 和 `justification` 字段,用于一次经批准的重试,该重试必须严格宽于会话的有效模式。授权仅放宽该次重试;拒绝则不执行任何内容,返回 `the user rejected escalating this command to "<mode>"`,且不允许再次请求。由归属方派生的待处理策略上下文会说明当前文件策略,但不会取代这些强制执行边界。当 `dsh-permission` 与某个 UI 适配器一起组合时,一个 preset 同时选定两个旋钮值;不匹配的组合折叠为 `custom`。[ACP 自动化组合](../../../../examples/acp-agent/README.md)不挂载该 UI 服务,而是显式选定其部署模式。
@@ -54,7 +54,7 @@ OS 子进程约束适用于 bash 执行器(包括钩子命令),后续还
策略随每次调用而非提供方携带:两个消费方可以在同一时刻以不同策略约束(bash 在 `read-only` 下运行,而一个受约束的子 agent 保持其状态目录可写),且经批准的升级重试是一次带有更宽策略的新调用——在配置固定的提供方模式下无法表达。
该 seam 仅约束与宿主机共享文件系统和内核的子进程。容器、microVM 和远程执行器不是此 seam 的后端——它们以环境一致的组替换整个能力实现(`ctx.bash`、`ctx.fs`),因为一个 bash 在容器中运行而 fs 工具写主机的 agent 生活在两个割裂的世界中。
该 seam 仅约束与宿主机共享文件系统和内核的子进程。容器、microVM 和远程执行器不是此 seam 的后端——它们以环境一致的组替换整个能力 seam(`ctx.bash`、`ctx.fs`)的 Service provider,因为一个 bash 在容器中运行而 fs 工具写主机的 agent 生活在两个割裂的世界中。
留待需要时再决定:网络限制是作为独立的 `network_mode` 到来,还是在某个 runner 同时强制两者后合并进 `sandbox_mode`;以及 `SandboxPolicy` 是现在就增加额外的可写根授权(launcher 已支持 `--rw <path>`),还是等到升级机制需要时再加。
@@ -70,7 +70,7 @@ Landlock launcher 源码和包家族位于 `native/landlock-run`,与 harness
#### bash 消费方
`dsh-bash-sandbox` 扩展 `LocalBashExecutor`,把精确的 `['bash', '-c', command]` argv 交给 `ctx.sandbox`,并直接 spawn 提供方返回的 argv。这样,随附的原生 runner 建立约束后,shell 语义与 `BASH_ENV` 仍由内层 Bash 处理。提供方错误原样传播。进程启动前,只有当调用方拥有的 workdir 经独立验证可用,并且 Node 报告 `ENOENT` 或 `EACCES`,且带有明确指向提供方 argv[0] 的来源信息时,拒绝才会归因于 runner;没有精确错误路径的裸 `syscall: 'spawn'`、其他错误码、无效 workdir、资源失败、无关 syscall 与无结构拒绝保留本地命令启动语义。前台执行会将可归因于 runner 的拒绝转为 `SANDBOX_UNAVAILABLE` 并附上原始详细信息;异步后台拒绝则盖章 `runnerFailed: true`、`denied: false`。如果 `SubprocessService` 同步抛出同样带有来源信息的 `ENOENT`/`EACCES` 形态,后台启动会抛出 `SANDBOX_UNAVAILABLE`;其他同步错误原样传播。进程启动后,前台与后台共用一个 runner 失败分类器:先排除信息性行,再要求规则的退出码门控与余下的一行致命诊断同时匹配。匹配结果优先于拒绝:前台执行抛出 `SANDBOX_UNAVAILABLE`,并以该致命行作为详细信息;结算后的 `BashProcess` 会盖章 `sandbox.runnerFailed`,bash 生产者再通过通用 `task_output` 渲染它。
`dsh-bash-sandbox` 扩展 `LocalBashExecutor`,把精确的 `['bash', '-c', command]` argv 交给 `ctx.sandbox`,并直接 spawn 提供方返回的 argv。这样,随附的原生 runner 建立约束后,shell 语义与 `BASH_ENV` 仍由内层 Bash 处理。提供方错误原样传播。进程启动前,只有当调用方拥有的 workdir 经独立验证可用,Node 报告 `ENOENT` 或 `EACCES`,并且错误符合以下一种形态时,才判定为 runner 失败:`error.path` 等于提供方返回的 `argv[0]`,同时 `syscall` 为 `'spawn'` 或精确的 `'spawn <runner>'`;或者 `error.path` 不存在,同时 `syscall` 为精确的 `'spawn <runner>'`。其他错误码、无效 workdir、资源失败、无关 syscall 与无结构拒绝保留本地命令启动语义。前台执行会将 runner 失败转为 `SANDBOX_UNAVAILABLE` 并附上原始详细信息;异步后台拒绝则盖章 `runnerFailed: true`、`denied: false`。如果 `SubprocessService` 同步抛出同样能指明 runner 的形态,后台启动会抛出 `SANDBOX_UNAVAILABLE`;其他同步错误原样传播。进程启动后,前台与后台共用一个 runner 失败分类器:先排除信息性行,再要求规则的退出码检查与余下的一行致命诊断同时匹配。匹配结果优先于拒绝:前台执行抛出 `SANDBOX_UNAVAILABLE`,并以该致命行作为详细信息;结算后的 `BashProcess` 会盖章 `sandbox.runnerFailed`,bash 生产者再通过通用 `task_output` 渲染它。
模型会在归属方派生的 `sandbox:policy` 上下文中看到当前有效的文件策略;静态工具描述则解释拒绝标记(`[sandbox: file access denied under <mode> mode]`),鼓励尝试可能被拒绝的命令,并禁止绕过拒绝重试。当升级字段被公布时,被拒绝的结果还会携带升级提示本身,使被认可的同轮次重试在决策点获得提示,而非依赖模型回忆描述(§ 升级机制)。[当前策略决策](2026-07-30-current-sandbox-policy-context.md)负责该上下文的理由与边界。
@@ -82,7 +82,7 @@ Landlock launcher 源码和包家族位于 `native/landlock-run`,与 harness
当约束执行器被挂载时,`bash` 公布配对的 `sandbox_permissions` 和 `justification` 字段。schema 暴露完整的封闭升级词汇,因为有效模式是按会话的;执行拒绝任何不严格宽于该调用有效模式的目标。批准在执行之前解析。`allowed-once` 仅将授权模式盖章到该请求上,而 `rejected`、`cancelled`、`unavailable`、缺失的 approval 服务或缺失的 agent 都以各自不同的结果文本失败关闭。授权不持久化。
升级是对被拒绝命令的同轮次重试,使用最窄的足够 `sandbox_permissions` 和一个 `justification`;批准提示是同意步骤。它必须基于实际的拒绝,除非会话已观察到相同的被拒绝访问;禁用或被拒绝的批准终结该命令。重试、批准决策和结果使用既有的工具和批准事件。`dsh-tool-bash` 拥有请求动作,因为执行器 seam 既没有 agent 也没有用户交互所需的 call id。
升级是对被拒绝命令的同轮次重试,使用最窄的足够 `sandbox_permissions` 和一个 `justification`;批准提示是同意步骤。它必须基于实际的拒绝,除非会话已观察到相同的被拒绝访问;禁用或被拒绝的批准终结该命令。重试、批准决策和结果使用既有的工具和批准事件。`dsh-tool-bash` 拥有请求动作,因为执行器 Service Definition 既没有 agent 也没有用户交互所需的 call id。
仍未决定:持久授权超出沙箱模式之外的作用域标识是什么——确切调用、路径、命令前缀、会话或时间窗口——这是公布 `allow_always` 选项之前必须回答的问题。
@@ -113,7 +113,7 @@ interface SessionEventMap {
#### 进程内工具
fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层面的策略。fs seam 现在通过沙箱提供方强制共享模式词汇(`dsh-fs-sandbox` 按模式限制 write/edit;见[跨工具族 fs 沙箱 Agent Note](2026-07-14-cross-family-fs-sandbox.md)),因此 `read-only`/`workspace-write` 对文件系统工具也是真实边界,而非仅限 bash 的近似。web/todo 仍不受限制(web 的唯一效果是网络,不在文件效果模式词汇内)。没有通用的按工具沙箱运行时:主机中介的工具仅通过返回主机验证的声明式效果来离开进程,那是一次重写而非包装——后续设计选择了一个共享策略归属 `ctx.sandboxPolicy`,由各 seam 强制,而不是统一包装器。
fs/web/todo 在进程内执行,因此它们的沙箱语义是各自能力边界层面的策略。fs seam 现在通过沙箱提供方强制共享模式词汇(`dsh-fs-sandbox` 按模式限制 write/edit;见[跨工具族 fs 沙箱 Agent Note](2026-07-14-cross-family-fs-sandbox.md)),因此 `read-only`/`workspace-write` 对文件系统工具也是真实边界,而非仅限 bash 的近似。web/todo 仍不受限制(web 的唯一效果是网络,不在文件效果模式词汇内)。没有通用的按工具沙箱运行时:主机中介的工具仅通过返回主机验证的声明式效果来离开进程,那是一次重写而非包装——后续设计选择了一个共享策略归属 `ctx.sandboxPolicy`,由各能力强制,而不是统一包装器。
### 测试
@@ -142,7 +142,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **提供方上的配置固定模式**:否决。每进程一个模式;无法服务具有不同策略的并发消费方,也无法表达一次性放宽重试。
- **一个接口同时覆盖容器/VM**:否决。`confine(argv)` 预设共享文件系统;环境隔离是作为一致组部署的能力兄弟后端。
- **通用 ToolRuntime 包装任何工具**:否决。对进程内工具(闭包了 `ctx`)机械上不成立;声明式效果重写对 fs/web/todo 而言不合理。
- **在执行器内部(`dsh-bash-sandbox`)请求批准**:否决。没有可路由的 `agent`,没有可附加提示词的 `callId`;添加它们会让传输 seam 了解会话和 UI——工具层持有两者并拥有面向模型的词汇。
- **在执行器内部(`dsh-bash-sandbox`)请求批准**:否决。没有可路由的 `agent`,没有可附加提示词的 `callId`;添加它们会让传输边界了解会话和 UI——工具层持有两者并拥有面向模型的词汇。
- **同一工具调用内自动重试**:否决。日志无法重建的隐藏重入:一个 `tool/call` 会产生两次具有不同策略的执行——重试是一次新的带有自身参数和结果事实的已记录调用。
- **无条件公布升级字段**:否决。在 `dsh-bash-local` 下它们是死杠杆——公布 harness 无法兑现的选项会制造注定失败的授权;能力门控仅需注册时一次读取。
- **默认值相对的升级阶梯(仅公布比执行器注册时默认值更宽的模式)**:否决。按会话覆盖使默认值成为错误的基线——切换到比默认值更窄的会话恰恰失去它需要的杠杆,而在 `danger-full-access` 默认值下字段完全消失,同时一个被覆盖为 `read-only` 的会话仍处于约束中却没有升级路径。枚举固定封闭的目标词汇;严格放宽是针对会话有效模式的按调用执行检查。
@@ -185,7 +185,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
## FAQ
- **一个命令返回了 `[sandbox: file access denied under read-only mode]`——它失败了吗?** 它运行了,内核拒绝了一个文件操作:拒绝是与退出码正交的结果事实。相关指令禁止通过绕过限制来重试;唯一被认可的动作是以升级请求重试同一命令一次。
- **如何区分损坏的沙箱与失败的命令?** 提供方 argv 的任何 spawn 拒绝都能证明受限启动从未开始,但只有在调用方拥有的 workdir 可用,且 Node 为该 argv[0] 报告可归因的 `ENOENT` 或 `EACCES` 时,才能据此判定 runner 损坏。没有精确错误路径的裸 `syscall: 'spawn'` 和其他所有拒绝仍是普通的命令启动错误。进程启动后,只有当 `runnerFailureRules` 中某一条目同时匹配其可选退出码门控,以及排除整行精确信息性行后的一行致命 stderr 诊断时,runner 失败才会优先于拒绝。前台失败会抛出结构化的 `SANDBOX_UNAVAILABLE`,并附带 spawn 错误或匹配行作为详细信息;遭异步拒绝或已结算的后台任务则盖章 `sandbox.runnerFailed` 并渲染自己的标记。如果 `SubprocessService` 同步抛出同样带有来源信息的 `ENOENT`/`EACCES` 形态,后台启动会抛出该结构化错误;其他同步错误原样传播。Landlock 部分强制执行通知加上普通子进程失败时,仍返回命令结果。
- **如何区分损坏的沙箱与失败的命令?** 提供方 argv 的任何 spawn 拒绝都能证明受限启动从未开始,但只有在调用方拥有的 workdir 可用,且 Node 为该 argv[0] 报告可归因的 `ENOENT` 或 `EACCES` 时,才能据此判定 runner 损坏。没有精确错误路径的裸 `syscall: 'spawn'` 和其他所有拒绝仍是普通的命令启动错误。进程启动后,只有当 `runnerFailureRules` 中某一条目同时匹配其可选退出码门控,以及排除整行精确信息性行后的一行致命 stderr 诊断时,runner 失败才会优先于拒绝。前台失败会抛出结构化的 `SANDBOX_UNAVAILABLE`,并附带 spawn 错误或匹配行作为详细信息;遭异步拒绝或已结算的后台任务则盖章 `sandbox.runnerFailed` 并渲染自己的标记。如果 `SubprocessService` 同步抛出同样带有 runner 路径的 `ENOENT`/`EACCES` 形态,后台启动会抛出该结构化错误;其他同步错误原样传播。Landlock 部分强制执行通知加上普通子进程失败时,仍返回命令结果。
- **在没有后端的平台上会发生什么——今天的 Windows?** `confine()` 抛出失败关闭的 `SANDBOX_UNAVAILABLE`,命令永不 spawn;`win32` 是保留的空链,由测试固定为同样失败关闭,直到 Windows runner 填充它(§ 延迟阶段)。
- **`bwrap` 已安装在我的主机上但不可用(禁用了非特权 userns、LSM 拒绝 `mount`)——会发生什么?** 链探测是功能性的——它构建并强制一个真实 profile 而非检查 `--version`——因此存在但不可用的 `bwrap` 探测失败,选择落到已打包的 Landlock launcher,结论在提供方生命周期内缓存。
- **沙箱限制网络或进程可见性吗?** 不——`SandboxMode` 仅声称文件操作;bwrap profile 刻意不 unshare pid,没有后端声称网络。网络限制是否成为自己的旋钮留在 § seam 中开放。
@@ -199,8 +199,8 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
本设计复制或对比的仓库内先例:
- [能力 seam Agent Note](../architecture/2026-06-13-capability-seams.md)——接口/实现/消费方拆分与「不要过早拆分」的时机规则(第二个消费方满足了该规则)。
- [能力 seam Agent Note](../architecture/2026-06-13-capability-seams.md)——Service Definition/Service provider/Consumer 拆分与「不要过早拆分」的时机规则(第二个消费方满足了该规则)。
- `dsh-bash` 的 request/spec 拆分([bash 词汇目录](../../../../docs/subsystems/bash.md))——完整的 `sandboxPolicy` 搭载其按调用载体,以及显式 `resolve()` 默认约定。
- [批准 seam Agent Note](2026-07-06-approval-seam.md)——升级请求通过的通道;其应答器 waterfall(瀑布式事件)、审计对和单包理由记录在那里。
- [事件溯源会话](../architecture/2026-06-11-event-sourced-sessions.md)与[独立纯日志事件](../simplification/2026-07-28-remove-synthetic-log-only-turns.md)——按会话模式 fold 所依赖的日志即存储基础,以及锚定设计遵守的显式持久性边界。
- [拦截 seam Agent Note](2026-06-30-interception-seams.md)——`tools/pre-execute` 词汇,升级门控刻意不复用它(升级调用没有自己的 pre-execute 时刻)。
- [拦截扩展点 Agent Note](2026-06-30-interception-extension-points.md)——`tools/pre-execute` 词汇,升级门控刻意不复用它(升级调用没有自己的 pre-execute 时刻)。

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 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
2026-07-08-self-referential-cordis-toolset.md: a2f614cc5a236e45622eae2b30f518181331cc79
2026-07-08-self-referential-cordis-toolset.zh.md: 6c56c2bf0c8cc8d175f451290620d9386bb69a23
2026-07-08-self-referential-cordis-toolset.md: 5fc2fb07fcd0b00bf72c818d3806b298312cdc31
2026-07-08-self-referential-cordis-toolset.zh.md: 8f34c97d94cad9b79a0e823406c07cdcfb38793f

View File

@@ -75,7 +75,7 @@ The correctness investment therefore goes where it pays for every capability at
**A hand-maintained service/event reference in the tool.** The first cut of the inspect tool carried a hand-written table of service method signatures. It was replaced by the generated `api-catalog.ts` because a hand table drifts from the JSDoc the moment a signature changes and nothing gates the drift, whereas the generated artifact is freshness-checked against the same AST the docs use.
**A new `cordis/mount` session event.** A durable provenance event recording each mount (source, name) has clear precedent (`hook/invoked`, `compact/start`). It was declined for v1: mount and unmount are already visible as `tool/call` / `tool/result` pairs and the tool-set change is already logged as a full changed request header, so a dedicated event would only duplicate the record. It remains addable if an audit use case needs mount provenance separable from the tool call.
**A new `cordis/mount` session event.** A durable event recording each mount's source and name has clear precedent (`hook/invoked`, `compact/start`). It was declined for v1: mount and unmount are already visible as `tool/call` / `tool/result` pairs and the tool-set change is already logged as a full changed request header, so a dedicated event would only duplicate the record. It remains addable if an audit use case needs the mount source and name outside the tool call.
**A hardened / capability-restricted sandbox.** Trapping Node built-ins and handing mount code a whitelist façade rather than the raw context might suggest an intent to sandbox for safety. It is explicitly not that: the traps and the façade narrow the *surface* mount code sees — steering it onto cordis services and away from leak-prone Node built-ins and framework internals — for correctness and to close the unguarded-context escape, but the capabilities the façade exposes (`ctx.bash`, `ctx.fs`, `ctx.web`) reach the real runtime, so it is not a security boundary. A real one (separate process, permission prompts) was out of scope for a dev/opt-in toolset and would fight the entire point — handing the model the live runtime.

View File

@@ -75,7 +75,7 @@ vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节
**在工具中手工维护服务/事件参考。** inspect 工具的第一版携带了一份手写的服务方法签名表。它被生成的 `api-catalog.ts` 取代,因为手写表在签名变化的瞬间就会与 JSDoc 脱节且没有门禁约束这种漂移,而生成产物的新鲜度由文档使用的同一套 AST 检查。
**新增 `cordis/mount` 会话事件。** 一个持久的溯源事件记录每次挂载(源码、名称)有明确先例(`hook/invoked`、`compact/start`)。v1 中予以否决:挂载和卸载已经作为 `tool/call` / `tool/result` 对可见,工具集变化已经作为完整的变更 request header 被记录,因此专用事件只会重复记录。如果审计用例需要将挂载溯源从工具调用中分离出来,日后仍可添加。
**新增 `cordis/mount` 会话事件。** 一个持久事件记录每次挂载的源码和名称,有明确先例(`hook/invoked`、`compact/start`)。v1 中予以否决:挂载和卸载已经作为 `tool/call` / `tool/result` 对可见,工具集变化已经作为完整的变更 request header 被记录,因此专用事件只会重复记录。如果审计用例需要在工具调用之外取得挂载的源码和名称,日后仍可添加。
**加固的/能力受限的沙箱。** 对 Node 内置模块设陷阱并向挂载代码提供白名单门面而非原始上下文,可能暗示意图是为安全而沙箱化。这里明确不是:陷阱和门面收窄的是挂载代码所见的*接口面*——将其引导至 cordis 服务、远离易泄漏的 Node 内置模块和框架内部——目的是正确性和封堵未受保护的上下文逃逸,但门面暴露的能力(`ctx.bash`、`ctx.fs`、`ctx.web`)触及真实运行时,因此它不是安全边界。真正的安全边界(独立进程、权限提示)超出了一个开发/显式启用工具集的范围,且会与其核心目的——将活跃运行时交给模型——相冲突。

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 .agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md
2026-07-10-parallel-tool-call-execution.md: 7fd540539fd358b3254470f420a0cc11f2927d45
2026-07-10-parallel-tool-call-execution.zh.md: 278996fedb8c9dcc86916a1544535b1444b6e9f5
2026-07-10-parallel-tool-call-execution.md: 830691b7596accd1c5746fc51e38a9022f09e501
2026-07-10-parallel-tool-call-execution.zh.md: bd336919adbf9b96f64477c1e24964172cde6d92

View File

@@ -82,7 +82,7 @@ Snapshot coverage pins the visible multi-call transcript: pending calls may over
**Parallelize the complete tool pipeline.** This keeps the loop on the public one-call API but runs pre- and post-execute middleware concurrently. Existing guards and hook bridges may carry ordered state, so only dispatch overlaps.
**Expose staged methods or a scheduling waterfall.** Public `prepare` / `dispatch` / `finalize` methods or a `tools/execution-mode` event add extension surface before another consumer needs it. The loop uses an internal scheduler view, while `executionMode(exec)` leaves an insertion point for a policy seam.
**Expose staged methods or a scheduling waterfall.** Public `prepare` / `dispatch` / `finalize` methods or a `tools/execution-mode` event add extension surface before another consumer needs it. The loop uses an internal scheduler view, while `executionMode(exec)` leaves an insertion point for a policy hook.
**Convert scheduler failures into tool results.** AgentLoop cannot determine whether a rejected dispatch invoked the tool body; ToolRegistry owns body-invocation state and typed tool outcomes. Internal scheduler failures therefore remain terminal instead of being reclassified as `ABORTED` results.

View File

@@ -82,7 +82,7 @@ Code Mode 仍不使用此调度器,因为模型只会发出一个原生 `run_c
**并行执行完整的工具流水线。** 这样可以让循环继续使用公开的单调用 API,但会并发运行 pre-execute 和 post-execute 中间件。现有防护和钩子桥可能承载有序状态,因此只允许派发重叠。
**公开分阶段方法或调度 waterfall。** 公开的 `prepare` / `dispatch` / `finalize` 方法或 `tools/execution-mode` 事件,会在出现另一个消费方之前扩大扩展接口。循环使用内部调度器视图,而 `executionMode(exec)` 为策略 seam 保留了插入点。
**公开分阶段方法或调度 waterfall。** 公开的 `prepare` / `dispatch` / `finalize` 方法或 `tools/execution-mode` 事件,会在出现另一个消费方之前扩大扩展接口。循环使用内部调度器视图,而 `executionMode(exec)` 为策略钩子保留了插入点。
**将调度器故障转换为工具结果。** AgentLoop 无法判断被拒绝的派发是否已调用工具主体;ToolRegistry 负责工具主体调用状态和类型化工具结果。因此,内部调度器故障保持为终态,而不会被重新分类为 `ABORTED` 结果。

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 .agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.md
2026-07-10-sqlite-session-query-provider.md: 372c21241f9ae5d7300165f016db9b36e6b52855
2026-07-10-sqlite-session-query-provider.zh.md: f5f2d22a209350975a4ccff9a538a55dfc8ff71e
2026-07-10-sqlite-session-query-provider.md: 0e15ea15f516091825276c4a9bcde2184c653123
2026-07-10-sqlite-session-query-provider.zh.md: caf700fd5979ff64c1445caba7fd44ffc28cb925

View File

@@ -16,7 +16,7 @@ Splitting those concerns across a provider coordinator and a database implementa
`@deepseek-ai/dsh-session-query-sqlite` extends the interface service and is the sole concrete owner of `ctx.sessionQuery`. It depends on live `ctx.sessions`, observes optional `ctx.sessionPersistence` dynamically, and owns a dedicated derived SQLite database. There is no search-provider registry, coordinator, persistence event, or agent-loop integration.
The interface package also owns shared first-party semantic extraction and provider-independent filtering. `SessionResultFilter` covers id, nullable cwd, created-at range, nullable parent, and availability; `ctx.sessionQuery.filterSessions()` applies it without an FTS provider. `SessionEventResultFilter` covers seq/time ranges, event type, surface, and literal semantic text. Arrays are ANDed and list values are ORed. The text clause escapes caller input into a Unicode case-insensitive regular expression whose whitespace runs match one or more whitespace characters; it is available through `ctx.sessionQuery.filterEvents()` and is not delegated to an FTS provider.
The Service Definition package also owns shared first-party semantic extraction and provider-independent filtering. `SessionResultFilter` covers id, nullable cwd, created-at range, nullable parent, and availability; `ctx.sessionQuery.filterSessions()` applies it without an FTS provider. `SessionEventResultFilter` covers seq/time ranges, event type, surface, and literal semantic text. Arrays are ANDed and list values are ORed. The text clause escapes caller input into a Unicode case-insensitive regular expression whose whitespace runs match one or more whitespace characters; it is available through `ctx.sessionQuery.filterEvents()` and is not delegated to an FTS provider.
## Search semantics

View File

@@ -16,7 +16,7 @@ Status: implemented
`@deepseek-ai/dsh-session-query-sqlite` 扩展接口服务,并且是 `ctx.sessionQuery` 唯一的具体所有者。它依赖实时的 `ctx.sessions`,动态观察可选的 `ctx.sessionPersistence`,并拥有一个专用的派生 SQLite 数据库。系统没有搜索提供方注册表、协调器、持久化事件或 agent loop(智能体循环)集成。
接口包还拥有共享的第一方语义提取与提供方无关的过滤。`SessionResultFilter` 涵盖 id、可空的 cwd、创建时间范围、可空的父会话与可用性;`ctx.sessionQuery.filterSessions()` 无需 FTS 提供方即可应用这些过滤器。`SessionEventResultFilter` 涵盖 seq/时间范围、事件类型、surface 与字面语义文本。过滤器数组内各项按逻辑与(AND)组合,列表值按逻辑或(OR)组合。文本子句会将调用方输入转义为不区分大小写的 Unicode 正则表达式,其中每段连续空白都匹配一个或多个空白字符;该子句通过 `ctx.sessionQuery.filterEvents()` 提供,不会委托给 FTS 提供方。
Service Definition 包还拥有共享的第一方语义提取与提供方无关的过滤。`SessionResultFilter` 涵盖 id、可空的 cwd、创建时间范围、可空的父会话与可用性;`ctx.sessionQuery.filterSessions()` 无需 FTS 提供方即可应用这些过滤器。`SessionEventResultFilter` 涵盖 seq/时间范围、事件类型、surface 与字面语义文本。过滤器数组内各项按逻辑与(AND)组合,列表值按逻辑或(OR)组合。文本子句会将调用方输入转义为不区分大小写的 Unicode 正则表达式,其中每段连续空白都匹配一个或多个空白字符;该子句通过 `ctx.sessionQuery.filterEvents()` 提供,不会委托给 FTS 提供方。
## 搜索语义

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 .agents/notes/implemented/feature/2026-07-13-session-query-tracing.md
2026-07-13-session-query-tracing.md: 47c12824a331546676d3bc79920f861afe648431
2026-07-13-session-query-tracing.zh.md: f578a69a3004722395d5b731f82482b8ebd548e5
2026-07-13-session-query-tracing.md: 66ec68cff116c973c723ee086cdb273f9375842b
2026-07-13-session-query-tracing.zh.md: 6a39a9857d30ea7911a313bff5504d4f5ae04562

View File

@@ -6,7 +6,7 @@ English | [中文](2026-07-13-session-query-tracing.zh.md)
## Problem
Session relationships are encoded across immutable headers, positional surface operations, and logged provenance arrays. A consumer reconstructing those relationships directly would need to duplicate corpus precedence, surface folding, malformed-log handling, deterministic lineage ordering, and cloning. Positional replacement and provenance are different graphs, so collapsing them into one generic edge type would also lose meaning.
Session relationships are encoded across immutable headers, positional surface operations, and logged arrays of cited source-event seqs. A consumer reconstructing those relationships directly would need to duplicate corpus precedence, surface folding, malformed-log handling, deterministic lineage ordering, and cloning. Positional replacement and cited-source relationships mean different things, so collapsing them into one generic edge type would also lose meaning.
## Decision
@@ -14,20 +14,20 @@ Session relationships are encoded across immutable headers, positional surface o
`SessionLineageTrace` returns the target, known parents in immediate-to-outward order, and recursive descendant trees whose siblings sort by creation time and then session id. `complete: true` carries the known root; `complete: false` carries the first unresolved parent id. A cycle connected to the target fails with `SESSION_QUERY_INVALID_LINEAGE`.
`SessionEventTrace` keeps positional and provenance relationships separate. `replacedBy` is the immediate positional replacer, `replacementChain` follows replacers to the final node, and `replacedEventSeqs` lists the actual surface nodes directly removed by the target. `sourceEventSeqs` preserves direct logged source order, while `derivedEventSeqs` lists later direct reverse references in log order. Provenance is not expanded transitively.
`SessionEventTrace` keeps positional replacements separate from cited source-event relationships. `replacedBy` is the immediate positional replacer, `replacementChain` follows replacers to the final node, and `replacedEventSeqs` lists the actual surface nodes directly removed by the target. `sourceEventSeqs` preserves direct logged source order, while `derivedEventSeqs` lists later direct reverse references in log order. The query does not follow cited source events transitively.
## Validation boundary
Event tracing checks target existence before surface analysis. Both event listing and tracing then use `dsh-session`'s one-pass surface fold, which accepts or rejects the loaded log as a whole: event seqs are zero-based and contiguous, surface markers obey event-type eligibility, provenance belongs only to surface event types, present arrays are nonempty and duplicate-free, every source is an earlier seq, and every positional replacement names and cites all surface nodes it removes. Every contract failure uses `SESSION_QUERY_INVALID_SURFACE`; there is no weaker classification-only surface standard.
Event tracing checks target existence before surface analysis. Both event listing and tracing then use `dsh-session`'s one-pass surface fold, which accepts or rejects the loaded log as a whole: event seqs are zero-based and contiguous, surface markers obey event-type eligibility, only surface event types may cite source-event seqs, present arrays are nonempty and duplicate-free, every source is an earlier seq, and every positional replacement names and cites all surface nodes it removes. Every contract failure uses `SESSION_QUERY_INVALID_SURFACE`; there is no weaker classification-only surface standard.
All returned records and arrays are detached. A known live event trace never consults persistence; persisted event traces preserve the exact-read list/load consistency check. Session lineage is necessarily a cross-corpus operation and therefore preserves cross-corpus persistence failure semantics.
## Alternatives considered
- **Expose standalone tracing helpers** — rejected because the source-precedence and detachment boundary belongs to `ctx.sessionQuery`; public helpers would invite callers to bypass it.
- **Combine replacement and provenance edges** — rejected because a positional replacement can shadow surface nodes while also citing non-surface construction inputs, and consumers need to distinguish those meanings.
- **Return transitive provenance closure** — rejected because it obscures logged direct evidence, increases result size, and lets one malformed distant edge alter otherwise local output.
- **Best-effort traces over malformed provenance** — rejected because a structurally plausible partial graph would look authoritative. Exact inspection fails loudly when the canonical relationship contract is broken.
- **Combine replacement and cited-source edges** — rejected because a positional replacement can shadow surface nodes while also citing non-surface construction inputs, and consumers need to distinguish those meanings.
- **Return all transitively cited source events** — rejected because it obscures logged direct evidence, increases result size, and lets one malformed distant edge alter otherwise local output.
- **Best-effort traces over malformed source-event lists** — rejected because a structurally plausible partial result would look authoritative. Exact inspection fails loudly when the canonical relationship contract is broken.
## Consequences

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
会话关系分散编码在不可变 header、位置式表面操作和已记录的来源数组中。消费方如果直接重建这些关系,就必须重复实现语料优先级、表面折叠、格式错误日志的处理、确定性的谱系顺序和克隆。位置替换与来源属于不同的图,因此把两者合并为一种通用边类型也会丢失含义。
会话关系分散编码在不可变 header、位置式表面操作和已记录的来源事件 seq 引用数组中。消费方如果直接重建这些关系,就必须重复实现语料优先级、表面折叠、格式错误日志的处理、确定性的谱系顺序和克隆。位置替换关系与来源事件引用关系表示不同含义,因此把两者合并为一种通用边类型也会丢失含义。
## 决策
@@ -14,20 +14,20 @@ Status: implemented
`SessionLineageTrace` 返回目标、按从直接父级到外层父级排序的已知父级,以及递归的后代树;同级节点先按创建时间排序,再按 session id 排序。`complete: true` 会携带已知根节点;`complete: false` 会携带第一个无法解析的父级 id。与目标相连的循环会以 `SESSION_QUERY_INVALID_LINEAGE` 失败。
`SessionEventTrace` 将位置关系与来源关系分开保留。`replacedBy` 是直接的位置替换者,`replacementChain` 沿替换者追踪至最终节点,`replacedEventSeqs` 则列出目标直接移除的真实表面节点。`sourceEventSeqs` 保留日志中直接来源的顺序,而 `derivedEventSeqs` 按日志顺序列出后续的直接反向引用。来源关系不会传递展开。
`SessionEventTrace` 将位置替换与来源事件引用关系分开保留。`replacedBy` 是直接的位置替换者,`replacementChain` 沿替换者追踪至最终节点,`replacedEventSeqs` 则列出目标直接移除的真实表面节点。`sourceEventSeqs` 保留日志中直接来源的顺序,而 `derivedEventSeqs` 按日志顺序列出后续的直接反向引用。查询不会继续传递追踪被引用的来源事件。
## 校验边界
事件追踪会在分析表面之前检查目标是否存在。随后,事件列表与追踪都会使用 `dsh-session` 的单遍表面折叠,对加载的日志整体进行接受或拒绝:事件 seq 从零开始且连续;表面标记符合事件类型的适用范围;只有表面事件类型可以携带来源;存在的数组必须非空且没有重复项;每个来源必须是更早的 seq;每次位置替换必须指明并引用它所移除的全部表面节点。任何约定违例都使用 `SESSION_QUERY_INVALID_SURFACE`;系统不存在只用于分类、要求更弱的表面标准。
事件追踪会在分析表面之前检查目标是否存在。随后,事件列表与追踪都会使用 `dsh-session` 的单遍表面折叠,对加载的日志整体进行接受或拒绝:事件 seq 从零开始且连续;表面标记符合事件类型的适用范围;只有表面事件类型可以引用来源事件 seq;存在的数组必须非空且没有重复项;每个来源必须是更早的 seq;每次位置替换必须指明并引用它所移除的全部表面节点。任何约定违例都使用 `SESSION_QUERY_INVALID_SURFACE`;系统不存在只用于分类、要求更弱的表面标准。
所有返回的记录与数组都与内部状态分离。已知的实时事件追踪绝不查询持久化;持久化事件追踪保留精确读取所要求的列表/加载一致性检查。会话谱系必然属于跨语料操作,因此也保留跨语料的持久化失败语义。
## 考虑过的替代方案
- **公开独立的追踪辅助函数**:不予采纳,因为源优先级与状态分离边界属于 `ctx.sessionQuery`;公开辅助函数会诱使调用方绕过该边界。
- **合并替换边与来源边**:不予采纳,因为位置替换可以遮蔽表面节点,同时引用不在表面上的构造输入,而消费方需要区分这两种含义。
- **返回传递来源闭包**:不予采纳,因为这会掩盖日志中直接记录的证据、增大结果,并让一条遥远的格式错误边改变原本局部的输出。
- **在格式错误的来源关系上返回尽力而为的追踪结果**:不予采纳,因为结构上看似合理的局部图会显得具有权威性。当规范的关系约定损坏时,精确检查会明确报错。
- **合并替换边与来源事件引用边**:不予采纳,因为位置替换可以遮蔽表面节点,同时引用不在表面上的构造输入,而消费方需要区分这两种含义。
- **返回所有传递引用的来源事件**:不予采纳,因为这会掩盖日志中直接记录的证据、增大结果,并让一条遥远的格式错误边改变原本局部的输出。
- **对格式错误的来源事件列表返回尽力而为的追踪结果**:不予采纳,因为结构上看似合理的局部结果会显得具有权威性。当规范的关系约定损坏时,精确检查会明确报错。
## 后果

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 .agents/notes/implemented/feature/2026-07-16-harness-level-loop.md
2026-07-16-harness-level-loop.md: f37bea0842b3f40bf07c6660ad758d84ece23f1b
2026-07-16-harness-level-loop.zh.md: 8d61c36915c7a3b5e0fde94c4c0ba52ec48eddd1
2026-07-16-harness-level-loop.md: 2efcf64adf07152b544c82f5bbf3bd8e415cd64d
2026-07-16-harness-level-loop.zh.md: 8b83ca4a7f957f304c093e4b3f4c462c6301d6bc

View File

@@ -19,7 +19,7 @@ This proposal is implemented in amended form as two explicit plugin policies ove
1. **Same-session goals** retain one durable objective in the current session and admit goal-attributed continuation turns only while live activation is armed.
2. **Fresh-agent Ralph runs** execute a fixed foreground workflow whose rounds each spawn a new structured child with no conversation seed.
There is no `packages/loop/` family, `LoopDriver`, `LoopId`, universal `StopCondition`, or model-facing generic `loop` tool. The two policies share the repository's ordinary agent, session, tools, workflow, subagent, and UI extension seams, but they do not pretend that one lifecycle fits both.
There is no `packages/loop/` family, `LoopDriver`, `LoopId`, universal `StopCondition`, or model-facing generic `loop` tool. The two policies share the repository's ordinary agent, session, tools, workflow, subagent, and UI extension points, but they do not pretend that one lifecycle fits both.
### Vocabulary and policy boundary
@@ -36,7 +36,7 @@ Time-based `/loop` or scheduled execution is a third policy and is not implement
| Package | Repository category | Owned structures and verbs |
|---|---|---|
| `@deepseek-ai/dsh-goal` | `packages/goal/goal/`, domain service | Owns `GoalId`, compare-and-set `GoalRef`, `GoalSnapshot`, four-state `GoalPhase`, structured `GoalBlockReason`, process-local `GoalActivation`, replay folding, and `get`, `create`, `edit`, `pause`, `resume`, `complete`, `block`, `clear`, and `disarm` verbs. |
| `@deepseek-ai/dsh-tool-goal` | `packages/goal/tool-goal/`, model-facing consumer | Registers exclusive `get_goal`, `create_goal`, and `update_goal`; authenticates live turn provenance and narrows autonomous-round authority to completion or blocking reports with machine-routable reason codes. |
| `@deepseek-ai/dsh-tool-goal` | `packages/goal/tool-goal/`, model-facing consumer | Registers exclusive `get_goal`, `create_goal`, and `update_goal`; requires a direct human message in a live root-agent turn and narrows autonomous-round authority to completion or blocking reports with machine-routable reason codes. |
| `@deepseek-ai/dsh-goal-session` | `packages/goal/goal-session/`, continuation policy | Reserves, fences, admits, attributes, settles, cancels, and quiescently drains same-session goal rounds without importing the concrete loop. |
| `@deepseek-ai/dsh-commands` | `packages/interaction/commands/`, UI registry | Owns `CommandDefinition`, discovery, scoped registration, direct dispatch, `CommandResult`, and request cancellation for human-only commands. |
| `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`, human-command producer | Registers `/goal` status, creation, edit, pause, resume, and clear over the goal domain for TUI. |
@@ -68,7 +68,7 @@ Normal turn completion schedules another round only while the goal remains activ
The human UX follows the compact Codex shape in the [public OpenAI Codex TUI dispatcher at commit `678157a`](https://github.com/openai/codex/blob/678157acaa819d5510adfe359abb5d0392cfe461/codex-rs/tui/src/chatwidget/slash_dispatch.rs#L750-L805): `/goal` shows status, `/goal <objective>` creates, and `edit`, `pause`, `resume`, or `clear` perform direct lifecycle actions. The commit permalink keeps the researched grammar verifiable as Codex evolves. Status includes durable phase, admitted/capped rounds, and live armed/disarmed activation. Direct status and command output do not enter model history; accepted domain mutations remain reconstructable because the goal service records them.
The model receives only `get_goal`, `create_goal`, and `update_goal`. It may create a goal when a direct human request clearly asks for substantial multi-round work, and it may infer that intent in any language. It must not turn routine one-turn work into a goal. Direct-human provenance is enforced in code; semantic interpretation remains model judgment. An autonomous goal round may report `complete` or `blocked` for the exact current goal round but cannot edit, pause, resume, or replace the human objective.
The model receives only `get_goal`, `create_goal`, and `update_goal`. It may create a goal when a direct human request clearly asks for substantial multi-round work, and it may infer that intent in any language. It must not turn routine one-turn work into a goal. Code requires a direct human message in the current live root-agent turn; semantic interpretation remains model judgment. An autonomous goal round may report `complete` or `blocked` for the exact current goal round but cannot edit, pause, resume, or replace the human objective.
TUI mounts the shared command registry and complete goal stack by default and exposes `/goal` through one producer. ACP mounts the goal domain, model tools, and same-session driver but deliberately omits the human command plane. Every effective registered command is discoverable and invocable through every composed command adapter; a plugin incompatible with an application omits its command producer from that composition rather than relying on registry-level surface masks. The UI-less agent spine is opt-in so one-shot callers do not silently become multi-round operations. The headless CLI and JSON-RPC front doors do not consume the command plane; ordinary human text can still authorize model goal tools when that stack is composed.
@@ -98,11 +98,11 @@ The six owning Agent Notes record unit, integration, process, snapshot, cancella
## Alternatives considered
- **Implement the original universal loop capability seam** — rejected because `Evaluator`, `BudgetPolicy`, `RoundHandoff`, `GoalReflector`, background task ownership, persistence, and scheduling do not form one coherent mandatory abstraction. Building all of them before their first concrete consumers would create broad speculative surface and duplicate existing session, workflow, subagent, and task machinery.
- **Implement the original universal loop capability** — rejected because `Evaluator`, `BudgetPolicy`, `RoundHandoff`, `GoalReflector`, background task ownership, persistence, and scheduling do not form one coherent mandatory abstraction. Building all of them before their first concrete consumers would create broad speculative surface and duplicate existing session, workflow, subagent, and task machinery.
- **Implement only same-session goals** — rejected because fresh-context iteration is materially different and is a valuable demonstration of the plugin architecture. Ralph belongs as a fixed workflow consumer with explicit context reset.
- **Put Ralph inside the goal-round driver** — rejected because same-session goals deliberately preserve one conversation while Ralph deliberately removes it. Combining them would make activation, replay, handoff, and UI state ambiguous.
- **Treat a fork as a fresh Ralph child** — rejected because a fork carries a conversation prefix. Fresh children plus workspace state and one explicit report are easier to bound and replay without a synthetic cancel record.
- **Copy Claude Code's evaluator into the first goal implementation** — rejected because a transcript-only model evaluator is one useful policy, not a generally trustworthy completion certificate. Deterministic evaluation and isolation must remain possible, so the evaluator is deferred until its authority and provider seam are designed.
- **Copy Claude Code's evaluator into the first goal implementation** — rejected because a transcript-only model evaluator is one useful policy, not a generally trustworthy completion certificate. Deterministic evaluation and isolation must remain possible, so the evaluator is deferred until its authority and provider contract are designed.
- **Automatically continue after session restore** — rejected because opening a session is observation, not authority to spend resources. Durable state is restored while activation waits for a new human prompt.
- **Route `/goal` through the model** — rejected because status and explicit lifecycle controls should be deterministic, token-free UI actions; ordinary natural-language prompts remain the semantic model path.
- **Modify the concrete agent loop with goal or Ralph modes** — rejected because public queue, prompt, session, cancellation, workflow, and subagent seams already support both policies. The generic cancel-requested observation is the only core coordination addition.
@@ -111,7 +111,7 @@ The six owning Agent Notes record unit, integration, process, snapshot, cancella
- Goal-based execution ships without one overloaded “loop” object: same-session continuation and fresh-agent iteration have explicit, separately testable contracts.
- Durable goal history is replayable and forkable, while process-local activation prevents accidental work on resume.
- Humans receive a small Codex-shaped UX; models receive a compact provenance-checked tool surface; deployments can remove either independently.
- Humans receive a small Codex-shaped UX; models receive a compact tool set whose mutating calls require a direct human message in the current live root-agent turn; deployments can remove either independently.
- Ralph demonstrates a nontrivial fixed policy entirely as a plugin over existing workflow and subagent primitives.
- Round limits are generous by default but remain deployment-controlled. They bound iterations, not tokens, price, elapsed time, or external side effects.
- The original proposal's evaluator, budget, reflector, background-task, CLI, and generic loop-session architecture is intentionally not part of the implemented public surface.
@@ -125,5 +125,5 @@ The six owning Agent Notes record unit, integration, process, snapshot, cancella
- **No generic loop journal or execution-world rewind** — session replay reconstructs goal history, not prior files, processes, environment, credentials, or external side effects. Ralph treats the current workspace as authority and carries no cross-run journal.
- **No goal reflector** — concern events, automatic no-progress heuristics, goal revision by an independent reflector, stuck-pattern detection, and `loop_split` are not implemented. Humans can edit, pause, clear, or resume the goal directly.
- **Ralph policy remains narrow** — one round creates one fresh child; within-round fan-out, evaluator/worker role separation, dynamic provider/model selection, and structural recursive-Ralph tool denial need separate policy surfaces. Prompt guidance is not enforcement.
- **Ralph does not retry a failed child** — an ordinary failure preserves the failed round and last good handoff, while fatal workflow infrastructure failures can end before that state is available. Retry count, backoff, and richer failure transport need separate policy and seam design.
- **Ralph does not retry a failed child** — an ordinary failure preserves the failed round and last good handoff, while fatal workflow infrastructure failures can end before that state is available. Retry count, backoff, and richer failure transport need separate policy and boundary design.
- **Portable UI remains modest** — TUI renders plain-text goal status and generic Ralph cards. ACP carries only committed assistant text; there is no continuous status widget, reconnectable command output, modal goal editor, or command plane in ACP, the headless CLI, or JSON-RPC.

View File

@@ -19,7 +19,7 @@ Status: implemented
1. **同会话目标**在当前会话中保留一个持久目标,并且只在实时激活态已激活时接纳带目标归属的续行轮次。
2. **全新 agent Ralph 运行**执行一个固定前台工作流,其中每个 Round 都生成一个不带对话种子的全新结构化子 agent。
系统中没有 `packages/loop/` 包族、`LoopDriver`、`LoopId`、通用 `StopCondition` 或面向模型的通用 `loop` 工具。两项策略共享本仓库普通的 agent、会话、工具、工作流、subagent 与 UI 扩展 seam,但不会假装一种生命周期可以同时适配两者。
系统中没有 `packages/loop/` 包族、`LoopDriver`、`LoopId`、通用 `StopCondition` 或面向模型的通用 `loop` 工具。两项策略共享本仓库普通的 agent、会话、工具、工作流、subagent 与 UI 扩展点,但不会假装一种生命周期可以同时适配两者。
### 词汇与策略边界
@@ -36,7 +36,7 @@ Status: implemented
| 包 | 仓库类别 | 所属结构与动词 |
|---|---|---|
| `@deepseek-ai/dsh-goal` | `packages/goal/goal/`,领域服务 | 拥有 `GoalId`、比较并交换 `GoalRef`、`GoalSnapshot`、四状态 `GoalPhase`、结构化 `GoalBlockReason`、进程本地 `GoalActivation`、重放折叠,以及 `get`、`create`、`edit`、`pause`、`resume`、`complete`、`block`、`clear` 与 `disarm` 动词。 |
| `@deepseek-ai/dsh-tool-goal` | `packages/goal/tool-goal/`,面向模型消费方 | 注册互斥的 `get_goal`、`create_goal` 与 `update_goal`;认证实时 Turn 来源,并把自治 Round 权限收窄到带机器可路由原因代码的完成或阻塞报告。 |
| `@deepseek-ai/dsh-tool-goal` | `packages/goal/tool-goal/`,面向模型消费方 | 注册互斥的 `get_goal`、`create_goal` 与 `update_goal`;要求实时根 agent Turn 中有一条人类直接发送的消息,并把自治 Round 权限收窄到带机器可路由原因代码的完成或阻塞报告。 |
| `@deepseek-ai/dsh-goal-session` | `packages/goal/goal-session/`,续行策略 | 在不导入具体 loop 的情况下,预留、设围栏、接纳、归属、结算、取消并排空同会话 Goal Round,直至完全停稳。 |
| `@deepseek-ai/dsh-commands` | `packages/interaction/commands/`,UI 注册表 | 拥有面向人类专用命令的 `CommandDefinition`、发现、作用域注册、直接分发、`CommandResult` 与请求取消。 |
| `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`,人类命令生产方 | 为 TUI 注册构建在目标领域之上的 `/goal` 状态、创建、编辑、暂停、恢复与清除。 |
@@ -68,7 +68,7 @@ Goal Round 驱动器为每个特定的实时 agent 至多拥有一个待定预
人类 UX 遵循 [OpenAI Codex 在提交 `678157a` 时的公开 TUI 分发器](https://github.com/openai/codex/blob/678157acaa819d5510adfe359abb5d0392cfe461/codex-rs/tui/src/chatwidget/slash_dispatch.rs#L750-L805)中的紧凑形态:`/goal` 显示状态,`/goal <objective>` 创建目标,而 `edit`、`pause`、`resume` 或 `clear` 执行直接生命周期操作。该提交永久链接让研究所得语法在 Codex 演进时仍可验证。状态包含持久阶段、已接纳/上限 Round 数以及实时已激活/未激活状态。直接状态与命令输出不会进入模型历史;已接受领域变更仍可重建,因为目标服务会记录它们。
模型只接收 `get_goal`、`create_goal` 和 `update_goal`。当直接人类请求清楚要求大量多 Round 工作时,模型可以创建目标,并且可以从任何语言推断该意图。它不得把日常单 Turn 工作变成目标。直接人类来源由代码强制执行;语义解释仍是模型判断。自治目标 Round 可以为确切的当前 Goal Round 报告 `complete` 或 `blocked`,但不能编辑、暂停、恢复或替换人类目标。
模型只接收 `get_goal`、`create_goal` 和 `update_goal`。当直接人类请求清楚要求大量多 Round 工作时,模型可以创建目标,并且可以从任何语言推断该意图。它不得把日常单 Turn 工作变成目标。代码要求当前实时根 agent Turn 中有一条人类直接发送的消息;语义解释仍是模型判断。自治目标 Round 可以为确切的当前 Goal Round 报告 `complete` 或 `blocked`,但不能编辑、暂停、恢复或替换人类目标。
TUI 默认挂载共享命令注册表和完整目标栈,并通过一个生产方暴露 `/goal`。ACP(Agent Client Protocol)挂载目标领域、模型工具和同会话驱动器,但有意省略人类命令平面。每条有效已注册命令都能被每个已组合的命令适配器发现和调用;若插件与某应用不兼容,该应用组合会省略其命令生产方,而不是依赖注册表层面的表面掩码。无 UI 的 agent 主干要求显式选择加入,以免单次调用方静默变成多 Round 操作。无头 CLI(命令行界面)与 JSON-RPC 前端不消费命令平面;挂载目标栈后,普通人类文本仍可授权模型目标工具。
@@ -98,11 +98,11 @@ Codex 提供了这里采用的最小可观察目标 UX:一个附着于聊天
## 考虑过的替代方案
- **实现原始通用 loop 能力 seam**——不予采纳,因为 `Evaluator`、`BudgetPolicy`、`RoundHandoff`、`GoalReflector`、后台任务所有权、持久化与调度并不构成一项一致的必选抽象。在出现首个具体消费者前全部构建,会产生宽泛推测性表面,并重复现有 session、workflow、subagent 与 task 机制。
- **实现原始通用 loop 能力**——不予采纳,因为 `Evaluator`、`BudgetPolicy`、`RoundHandoff`、`GoalReflector`、后台任务所有权、持久化与调度并不构成一项一致的必选抽象。在出现首个具体消费者前全部构建,会产生宽泛推测性表面,并重复现有 session、workflow、subagent 与 task 机制。
- **只实现同会话目标**——不予采纳,因为全新上下文迭代在实质上不同,也是插件架构的重要示范。Ralph 应作为带显式上下文重置的固定工作流消费者。
- **把 Ralph 放进目标回合驱动器**——不予采纳,因为同会话目标有意保留一段对话,而 Ralph 有意移除对话。合并两者会让激活、重放、交接与 UI 状态含糊不清。
- **把 fork 当成全新 Ralph 子 agent**——不予采纳,因为 fork 会携带对话前缀。全新子 agent 加工作区状态与一份显式报告更容易限制和重放,并且无需合成取消记录。
- **把 Claude Code 评估器复制进首个目标实现**——不予采纳,因为只读取转录的模型评估器是一项有用策略,但不是普遍可信的完成证书。系统必须仍能支持确定性评估与隔离,因此评估器延期到其权限与 provider seam 完成设计之后。
- **把 Claude Code 评估器复制进首个目标实现**——不予采纳,因为只读取转录的模型评估器是一项有用策略,但不是普遍可信的完成证书。系统必须仍能支持确定性评估与隔离,因此评估器延期到其权限与提供方约定完成设计之后。
- **会话恢复后自动续行**——不予采纳,因为打开会话是观察行为,不是花费资源的权限。系统恢复持久状态,而激活态等待新的人类提示词。
- **通过模型路由 `/goal`**——不予采纳,因为状态与显式生命周期控制应是确定性且零 token 的 UI 操作;普通自然语言提示词仍是语义模型路径。
- **为具体 agent loop 增加目标或 Ralph 模式**——不予采纳,因为公开队列、提示词、会话、取消、工作流与 subagent seam 已经支持两项策略。通用 cancel-requested 观察是唯一核心协调新增项。
@@ -111,7 +111,7 @@ Codex 提供了这里采用的最小可观察目标 UX:一个附着于聊天
- 目标式执行在没有单个过载「loop」对象的情况下交付:同会话续行与全新 agent 迭代拥有显式、可独立测试的约定。
- 持久目标历史可以重放和 fork,而进程本地激活态会防止恢复时意外开始工作。
- 人类获得小型 Codex 形态 UX;模型获得紧凑、带来源检查的工具表面;部署可以独立移除任一能力。
- 人类获得小型 Codex 形态 UX;模型获得一组紧凑工具,其中的修改操作要求当前实时根 agent Turn 中有一条人类直接发送的消息;部署可以独立移除任一能力。
- Ralph 展示了非平凡固定策略可以完全作为现有 workflow 与 subagent 原语之上的插件实现。
- Round 上限默认宽裕,但仍由部署控制。它限制迭代次数,不限制 token、价格、耗时或外部副作用。
- 原始提案中的评估器、预算、反思器、后台任务、CLI 与通用 loop-session 架构有意不进入已实现公开表面。
@@ -125,5 +125,5 @@ Codex 提供了这里采用的最小可观察目标 UX:一个附着于聊天
- **没有通用 loop 日志或执行世界回退**——会话重放会重建目标历史,而不会恢复此前文件、进程、环境、凭据或外部副作用。Ralph 把当前工作区作为权威,并且没有跨运行日志。
- **没有目标反思器**——concern 事件、自动无进展启发式、由独立反思器执行的目标修订、卡住模式检测与 `loop_split` 均未实现。人类可以直接编辑、暂停、清除或恢复目标。
- **Ralph 策略仍然狭窄**——一个 Round 创建一个全新子 agent;Round 内扇出、评估器/工作者角色分离、动态 provider/模型选择与从结构上禁止递归调用 Ralph 工具都需要独立策略表面。提示词指导不是强制执行。
- **Ralph 不会重试失败的子 agent**——普通失败会保留失败 Round 与上一份有效交接,而致命工作流基础设施错误可能在该状态可用前结束。重试次数、退避与更丰富的失败传输需要独立的策略与 seam 设计。
- **Ralph 不会重试失败的子 agent**——普通失败会保留失败 Round 与上一份有效交接,而致命工作流基础设施错误可能在该状态可用前结束。重试次数、退避与更丰富的失败传输需要独立的策略与边界设计。
- **可移植 UI 仍较朴素**——TUI 渲染纯文本目标状态和通用 Ralph 卡片。ACP 只承载已提交的助手文本;系统没有持续状态组件、可重连命令输出、模态目标编辑器,ACP、无头 CLI 与 JSON-RPC 也没有命令平面。

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 .agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md
2026-07-16-persistent-pty-sessions.md: 219f48235d527cffbf66020ceb22e80ca473093d
2026-07-16-persistent-pty-sessions.zh.md: c3842936248fb2d1705708dfd694be563ce2adf9
2026-07-16-persistent-pty-sessions.md: 5d45f5fd13099352d503bc3296e4f6b7b0c53bc8
2026-07-16-persistent-pty-sessions.zh.md: a0b4150157110b64943a42ce4e3544342e1be788

View File

@@ -26,7 +26,7 @@ The implementation supports interactive shells and line-oriented REPLs on Linux
| `dsh-pty-local` | Persistent-shell backend over `ctx.subprocess.spawnTerminal()`: readiness, bounded terminal buffers, sandbox resolution, and owner-aware session lifecycle | registers a backend on `ctx.pty` |
| `dsh-tool-pty` | Six model-facing tools, task-runtime integration for background sends, guidance, and UI render intents | registers on `ctx.tools` |
Readiness remains PTY-backend behavior, not a second public seam. The terminal-process provider supplies only substrate facts such as the foreground process group and whether it can prove that group is waiting on input; `dsh-pty-local` combines those facts with prompt and silence evidence into the common send result.
Readiness remains PTY-backend behavior, not a second public contract. The terminal-process provider supplies only substrate facts such as the foreground process group and whether it can prove that group is waiting on input; `dsh-pty-local` combines those facts with prompt and silence evidence into the common send result.
### Agent ownership and identity
@@ -64,7 +64,7 @@ The UI render contract is exact and location-free. `terminal_send` uses terminal
Foreground sends return a bounded rendered delta and two independent facts: `waitReason` (`stdin_read | inferred_idle | timeout | session_exit`) and `sessionStatus` (`running` or `exited` with exit code or signal). `session_exit` refers to the PTY's top-level shell process, not an arbitrary foreground command whose status the shell consumes. A timeout never implies process exit. `dsh-tool-pty.maxResultBytes` defaults to 262144, rejects values below 64 so creation acknowledgements retain registry-issued ids, and caps each single-text UTF-8 result after normalized tool or pipeline errors, wait, session, pagination, truncation, generic task-status wrappers, policy denials or short-circuits, and post-execute replacements or blocks; the terminal definitions' last-mile `finalizeContent` callback leaves deliberately structured multi-block policy content unchanged. The renderer reserves suffix space and preserves code-point boundaries instead of treating the backend payload cap as the final model bound.
With `run_in_background: true`, `dsh-tool-pty` registers the in-flight send on `ctx.tasks` and returns immediately with `taskId`. The producer places `maxResultBytes` on the task snapshot so `task_output`, terminal kill status, and completion notices enforce the same complete-result cap after generic metadata. `task_output(wait: true)` waits, reads incremental output, and records the final result; `task_kill` resolves the current foreground PGID and delivers a real `SIGINT`, including when the application has disabled terminal `ISIG`, and escalates only through the PTY backend's owned teardown path. If the task surface is absent, background mode fails before writing input. No PTY-specific `sleep` tool or general wake-up seam is added.
With `run_in_background: true`, `dsh-tool-pty` registers the in-flight send on `ctx.tasks` and returns immediately with `taskId`. The producer places `maxResultBytes` on the task snapshot so `task_output`, terminal kill status, and completion notices enforce the same complete-result cap after generic metadata. `task_output(wait: true)` waits, reads incremental output, and records the final result; `task_kill` resolves the current foreground PGID and delivers a real `SIGINT`, including when the application has disabled terminal `ISIG`, and escalates only through the PTY backend's owned teardown path. If the task surface is absent, background mode fails before writing input. No PTY-specific `sleep` tool or general wake-up API is added.
`terminal_read` pages backward from the newest retained line. The backend enforces both line and UTF-8 byte caps on retained scrollback and the returned page payload, so one oversized line cannot bypass the backend bound; the tool then caps the fully rendered page including pagination and truncation metadata. `truncated` distinguishes retention loss from an ordinary viewport delta.

View File

@@ -26,7 +26,7 @@ harness 可以运行前台与后台命令、编辑文件和委派工作,但无
| `dsh-pty-local` | 基于 `ctx.subprocess.spawnTerminal()` 的持久 shell 后端:就绪状态、有界终端缓冲、沙箱解析和感知 owner 的会话生命周期 | 在 `ctx.pty` 上注册后端 |
| `dsh-tool-pty` | 6 个面向模型的工具、后台发送的 task 运行时集成、使用指引和 UI 渲染意图 | 注册到 `ctx.tools` |
就绪判定仍属于 PTY 后端行为,不是第二条公共 seam。终端进程提供方只提供基底事实,例如前台进程组,以及能否证明该组正在等待输入;`dsh-pty-local` 将这些事实与提示符和静默证据组合成统一的发送结果。
就绪判定仍属于 PTY 后端行为,不是第二条公共约定。终端进程提供方只提供基底事实,例如前台进程组,以及能否证明该组正在等待输入;`dsh-pty-local` 将这些事实与提示符和静默证据组合成统一的发送结果。
### agent 所有权与身份
@@ -64,7 +64,7 @@ UI 渲染约定精确且不携带位置信息。`terminal_send` 只为前台发
前台发送返回有界的渲染增量和两个独立事实:`waitReason`(`stdin_read | inferred_idle | timeout | session_exit`)与 `sessionStatus`(`running`,或携带退出码或信号的 `exited`)。`session_exit` 指 PTY 顶层 shell 进程退出,不指由 shell 消费状态的任意前台命令。timeout 从不意味着进程已经退出。`dsh-tool-pty.maxResultBytes` 默认为 262144;低于 64 的值会被拒绝,以确保创建确认保留 registry 签发的 id;每个单文本 UTF-8 结果在加入规范化的工具或流水线错误、等待、会话、分页、截断、通用 task 状态包装、策略拒绝或短路以及 post-execute 替换或阻断后,仍受该值限制;终端定义自有的末端 `finalizeContent` callback 会原样保留策略刻意返回的结构化多 block 内容。渲染器会为后缀预留空间并保持代码点边界,而不会把后端载荷上限当作面向模型结果的最终上限。
当 `run_in_background: true` 时,`dsh-tool-pty` 在 `ctx.tasks` 上注册进行中的发送,并立即返回 `taskId`。生产方把 `maxResultBytes` 写入 task 快照,使 `task_output`、kill 返回的终态状态和完成通知在加上通用元数据后,仍对完整结果执行同一上限。`task_output(wait: true)` 负责等待、读取增量输出并记录最终结果;`task_kill` 会解析当前前台 PGID 并发送真正的 `SIGINT`,即使应用已禁用终端 `ISIG` 也同样如此,且后续升级仍只通过 PTY 后端拥有的 teardown 路径进行。若 task 对外接口不存在,后台模式必须在写入输入前失败。设计不新增 PTY 专用的 `sleep` 工具或通用唤醒 seam。
当 `run_in_background: true` 时,`dsh-tool-pty` 在 `ctx.tasks` 上注册进行中的发送,并立即返回 `taskId`。生产方把 `maxResultBytes` 写入 task 快照,使 `task_output`、kill 返回的终态状态和完成通知在加上通用元数据后,仍对完整结果执行同一上限。`task_output(wait: true)` 负责等待、读取增量输出并记录最终结果;`task_kill` 会解析当前前台 PGID 并发送真正的 `SIGINT`,即使应用已禁用终端 `ISIG` 也同样如此,且后续升级仍只通过 PTY 后端拥有的 teardown 路径进行。若 task 对外接口不存在,后台模式必须在写入输入前失败。设计不新增 PTY 专用的 `sleep` 工具或通用唤醒 API。
`terminal_read` 从最新保留行向后分页。后端同时对保留的 scrollback 和返回页载荷执行行数与 UTF-8 字节上限,因此单个超长行无法绕过后端上限;工具随后再限制包含分页与截断元数据的完整渲染页。`truncated` 用于区分保留数据丢失与普通 viewport 增量。

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 .agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md
2026-07-19-fresh-agent-ralph-workflow-tool.md: 6fe96587c49ef0316d1618fda2ee26b015b1ce87
2026-07-19-fresh-agent-ralph-workflow-tool.zh.md: 7cd3cf388f9d1e03390dd08ed045562bcfeb1f4d
2026-07-19-fresh-agent-ralph-workflow-tool.md: bbc8aba54bbd2cbfe9d9ba1fcc8cf9ad5c257d4f
2026-07-19-fresh-agent-ralph-workflow-tool.zh.md: 7ac405c2f6f80fa3151fb8b88c266ae2b83ea3eb

View File

@@ -12,7 +12,7 @@ Adding Ralph behavior to `dsh-agent-loop`, the goal driver, or the public model-
## Decision
Add `@deepseek-ai/dsh-tool-ralph` as a separate consumer package under `packages/workflow/`. It registers `ralph({ objective, maxRounds? })`, owns a fixed workflow script, and depends only on `ctx.tools`, `ctx.systemPrompt`, `ctx.workflows`, and `ctx.subagents`. A Ralph run is not a session goal, creates no goal state, and requires no branch in the concrete agent loop.
Add `@deepseek-ai/dsh-tool-ralph` as a separate Consumer package under `packages/workflow/`. It registers `ralph({ objective, maxRounds? })`, owns a fixed workflow script, and depends only on `ctx.tools`, `ctx.systemPrompt`, `ctx.workflows`, and `ctx.subagents`. A Ralph run is not a session goal, creates no goal state, and requires no branch in the concrete agent loop.
The tool is foreground-only. The calling agent parents every child for cwd and lineage, the parent tool call waits for the complete run, and the parent step's abort signal cancels the workflow. `run.dispose()` is awaited on every path, so cancellation reaches the worker engine's bounded settlement and child quiescence before the call returns.
@@ -70,5 +70,5 @@ A keyless real-stack integration drives the fixed script through the actual work
- Runs are foreground and process-local. Background collection, persistence/resume, scheduling, and restart recovery are absent.
- Round count is the only aggregate budget. Token, currency, elapsed-time, and provider-usage budgets remain separate future policy.
- One round creates one child. Within-round fan-out, evaluator/worker role separation, dynamic provider or model selection, and cross-run journals are deferred.
- An ordinary child failure ends the run without retry, while preserving the failed round and last successful handoff. Fatal workflow infrastructure failures can end before the fixed script returns that state; adding retry or richer failure transport requires separate policy and seam design.
- An ordinary child failure ends the run without retry, while preserving the failed round and last successful handoff. Fatal workflow infrastructure failures can end before the fixed script returns that state; adding retry or richer failure transport requires separate policy and boundary design.
- Prompt guidance asks models not to invoke Ralph recursively; a structural child-tool restriction would require a separately designed workflow child-policy surface.

View File

@@ -12,7 +12,7 @@ Status: implemented
## 决策
在 `packages/workflow/` 下新增独立的消费方包 `@deepseek-ai/dsh-tool-ralph`。它注册 `ralph({ objective, maxRounds? })`,拥有固定工作流脚本,并且只依赖 `ctx.tools`、`ctx.systemPrompt`、`ctx.workflows` 和 `ctx.subagents`。Ralph 运行不是会话目标,不会创建目标状态,也不要求在具体 agent loop 中增加分支。
在 `packages/workflow/` 下新增独立的 Consumer 包 `@deepseek-ai/dsh-tool-ralph`。它注册 `ralph({ objective, maxRounds? })`,拥有固定工作流脚本,并且只依赖 `ctx.tools`、`ctx.systemPrompt`、`ctx.workflows` 和 `ctx.subagents`。Ralph 运行不是会话目标,不会创建目标状态,也不要求在具体 agent loop 中增加分支。
该工具仅以前台方式运行。调用 agent 作为每个子 agent 的父级以提供 cwd 和谱系,父工具调用等待整次运行结束,父步骤的中止信号会取消工作流。每条路径都会等待 `run.dispose()`,因此调用返回前,取消会经过工作线程引擎的有界收敛并使子 agent 完全停稳。
@@ -70,5 +70,5 @@ Ralph 插件的 `subagentProvider` 默认为 `spawn`。每次调用前,它要
- 运行位于前台且只存在于进程内。后台收集、持久化/恢复、调度和重启恢复均不存在。
- Round 数是唯一聚合预算。token、货币、耗时和提供方用量预算仍属于未来的独立策略。
- 每个 Round 创建一个子 agent。Round 内扇出、evaluator/工作者角色分离、动态提供方或模型选择,以及跨运行日志均被推迟。
- 普通子 agent 失败会结束运行且不重试,同时保留失败 Round 与上一份成功交接。致命工作流基础设施错误可能在固定脚本返回该状态前结束;增加重试或更丰富的失败传输需要独立的策略与 seam 设计。
- 普通子 agent 失败会结束运行且不重试,同时保留失败 Round 与上一份成功交接。致命工作流基础设施错误可能在固定脚本返回该状态前结束;增加重试或更丰富的失败传输需要独立的策略与边界设计。
- 提示指导模型不要递归调用 Ralph;结构化的子 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 .agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md
2026-07-19-model-facing-goal-tools.md: 0271194a38503711de77290c915010c26a9de74b
2026-07-19-model-facing-goal-tools.zh.md: 097aaa3b160a394ff3d13681be04dd19c7856ebc
2026-07-19-model-facing-goal-tools.md: c2f1210ea636b61e99111d261cf9302c77576b46
2026-07-19-model-facing-goal-tools.zh.md: fd8ca90ba3b6809a7225b118ec6d311fb193cc8f

View File

@@ -12,7 +12,7 @@ The surface also needs to preserve the separation between durable state and live
## Decision
`@deepseek-ai/dsh-tool-goal` in `packages/goal/tool-goal/` contributes three exclusive tools and one system-prompt policy section over `ctx.goals`: `get_goal`, `create_goal`, and `update_goal`. The names and read-create-update shape follow Codex's compact goal tool surface while the authority rules use this repository's public agent, session, tool, and goal seams.
`@deepseek-ai/dsh-tool-goal` in `packages/goal/tool-goal/` contributes three exclusive tools and one system-prompt policy section over `ctx.goals`: `get_goal`, `create_goal`, and `update_goal`. The names and read-create-update shape follow Codex's compact goal tool surface while the authority rules use this repository's public agent, session, tool, and goal services.
### Tools and model contract
@@ -28,7 +28,7 @@ An autonomous goal round that successfully reports completion or blocking defers
Every call requires an `exec.agent` that is the exact running object in `AgentRegistry`, is the current inherited driver initiator, and has an open turn. These are execution-time checks and cannot be bypassed by prompt injection or hand-authored tool arguments.
Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. User source is a host attestation: every `Agent.followup()` or `steer()` input requires an explicit source, so the host labels direct human content `{ kind: 'user' }` and non-human producers label their own provenance. The runtime proves provenance, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model.
Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. User source is a host attestation: every `Agent.followup()` or `steer()` input requires an explicit source, so the host labels direct human content `{ kind: 'user' }` and non-human producers identify themselves in their source fields. The runtime proves that the current turn contains a direct human message, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model.
Complete and blocked accept either direct-human authority or the exact current goal round. Goal-round authority requires a goal-sourced `user/message` whose goal id, revision, and round all equal the folded current goal. It grants only the two terminal reports. Direct human authority may stop a goal immediately.
@@ -44,7 +44,7 @@ Unit coverage pins registration and disposal, exclusive scheduling, generated pr
- **Rely on prompt instructions for authority** — rejected because text can guide model judgment but cannot authenticate the live caller, turn, or source event.
- **Expose every goal-service verb as a separate tool** — rejected because a compact read/create/update surface reduces schema cost and keeps compare-and-set behavior uniform.
- **Require exact command phrases** — rejected because natural-language intent, including languages other than English, should be interpreted by the model; execution authority depends on provenance rather than spelling.
- **Require exact command phrases** — rejected because natural-language intent, including languages other than English, should be interpreted by the model; execution authority depends on a direct human message in the current turn rather than spelling.
- **Authorize from persisted root or fork metadata** — rejected because a fork that becomes an independently resumed top-level session should accept new human authority, while a currently owned child should not.
- **Let autonomous rounds edit or resume the goal** — rejected because continuation authority is narrower than authority to redefine or restart the human objective.
- **Treat the blocked threshold as an evaluator** — rejected because event counts cannot prove that an obstacle is semantically unchanged or truly terminal.
@@ -53,7 +53,7 @@ Unit coverage pins registration and disposal, exclusive scheduling, generated pr
## Consequences
- Models receive a stable, compact lifecycle surface without direct access to the goal service.
- State-changing calls are constrained by live runtime provenance as well as durable compare-and-set references.
- State-changing calls require a live runtime-root agent and a direct human message in the current turn, as well as durable compare-and-set references.
- Human requests can create and rearm goals through ordinary natural language, while restored sessions remain inert until such input arrives.
- Goal rounds can finish or report a repeated blocker but cannot broaden their own mandate.
- Deployment policy selects the blocking lower bound; the same resolved value controls enforcement and prompt guidance.

View File

@@ -12,7 +12,7 @@ Status: implemented
## 决策
位于 `packages/goal/tool-goal/` 的 `@deepseek-ai/dsh-tool-goal` 在 `ctx.goals` 之上贡献三个独占工具和一个系统提示词策略段:`get_goal`、`create_goal` 与 `update_goal`。工具名称和读取—创建—更新形态遵循 Codex 的紧凑目标工具表面,而权限规则使用本仓库公共的 agent(智能体)、会话、工具与目标 seam。
位于 `packages/goal/tool-goal/` 的 `@deepseek-ai/dsh-tool-goal` 在 `ctx.goals` 之上贡献三个独占工具和一个系统提示词策略段:`get_goal`、`create_goal` 与 `update_goal`。工具名称和读取—创建—更新形态遵循 Codex 的紧凑目标工具表面,而权限规则使用本仓库公共的 agent(智能体)、会话、工具与目标服务。
### 工具与模型约定
@@ -28,7 +28,7 @@ Status: implemented
每次调用都要求存在 `exec.agent`,且它必须是 `AgentRegistry` 中完全相同的运行中对象、当前继承的驱动发起者,并处于开放轮次内。这些检查在执行时进行,不能通过提示词注入或手写工具参数绕过。
创建、编辑、暂停与恢复还要求运行时根 agent 的当前轮次已经接纳一条用户消息或用户 steering 事件。根所有权来自实时 agent 图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子级则仍是 subagent,不能改变这些状态。用户来源是宿主的证明:每个 `Agent.followup()` 或 `steer()` 输入都必须显式提供来源,因此宿主把直接人类内容标为 `{ kind: 'user' }`,非人类生产者则标注自己的来源信息。运行时证明来源,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。
创建、编辑、暂停与恢复还要求运行时根 agent 的当前轮次已经接纳一条用户消息或用户 steering 事件。根所有权来自实时 agent 图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子级则仍是 subagent,不能改变这些状态。用户来源是宿主的证明:每个 `Agent.followup()` 或 `steer()` 输入都必须显式提供来源,因此宿主把直接人类内容标为 `{ kind: 'user' }`,非人类生产者则在来源字段中注明自己。运行时证明当前轮次包含人类直接发送的消息,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。
完成与阻塞既接受直接人类权限,也接受准确的当前 Goal Round。Goal Round 权限要求存在一条来源为目标的 `user/message`,其中目标 id、修订号和 Round 都与折叠后的当前目标相等。它只授予这两种终止报告权限。直接人类权限可以立即停止目标。
@@ -44,7 +44,7 @@ Status: implemented
- **依赖提示词指令实施权限**——不予采纳,因为文本可以指导模型判断,却不能认证实时调用者、轮次或来源事件。
- **把每个目标服务动词分别暴露为工具**——不予采纳,因为紧凑的读取/创建/更新表面可以降低模式成本,并保持统一的比较并交换行为。
- **要求精确命令短语**——不予采纳,因为自然语言意图(包括英语以外的语言)应由模型解释;执行权限取决于来源,而不是拼写。
- **要求精确命令短语**——不予采纳,因为自然语言意图(包括英语以外的语言)应由模型解释;执行权限取决于当前轮次中人类直接发送的消息,而不是拼写。
- **根据持久的根或派生元数据授权**——不予采纳,因为成为独立恢复顶层会话的派生应接受新的人类权限,而当前仍受所有权约束的子智能体则不应接受。
- **允许自主回合编辑或恢复目标**——不予采纳,因为继续执行权限比重新定义或重启人类目标的权限更窄。
- **把阻塞阈值当作评估器**——不予采纳,因为事件计数无法证明障碍在语义上未改变或确实不可继续。
@@ -53,7 +53,7 @@ Status: implemented
## 后果
- 模型获得稳定而紧凑的生命周期表面,无需直接访问目标服务。
- 改变状态的调用同时受到实时运行时来源与持久比较并交换引用的约束。
- 改变状态的调用要求实时运行时根 agent、当前轮次中人类直接发送的消息,以及持久比较并交换引用。
- 人类可以通过普通自然语言请求创建和重新激活目标,而恢复后的会话在收到此类输入前保持静止。
- Goal Round 可以完成或报告重复阻塞,但不能自行扩大任务权限。
- 部署策略选择阻塞下限;同一个解析后的值同时控制执行与提示词指导。

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 .agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
2026-07-19-persisted-same-session-goal-domain.md: ce93a652f3910912fbf6afc28061de4d13eb2de2
2026-07-19-persisted-same-session-goal-domain.zh.md: ddb4fdedafbacbfec355a7a1c912a5f9545e208a
2026-07-19-persisted-same-session-goal-domain.md: cdbbdd55c8c9b73851d5e729492656eda1380430
2026-07-19-persisted-same-session-goal-domain.zh.md: 7c8af321ec78779ecbf4e5e00805ef6f152b2aa1

View File

@@ -8,7 +8,7 @@ English | [中文](2026-07-19-persisted-same-session-goal-domain.zh.md)
A long-running objective outlives one prompt, turn, or model request. Treating that objective as an in-memory loop variable loses it on process restart, while putting it only in UI state makes model behavior impossible to reconstruct. Treating every session turn as progress also charges unrelated human messages against an automatic-work budget.
Durable lifecycle and permission to continue are different facts. A session may retain an active objective after restart or fork, but silently starting work when a user opens that session is surprising. The domain needs replayable state without persisted auto-execution authority, and it must remain a plugin on the public agent/session seams rather than a special case in the concrete loop.
Durable lifecycle and permission to continue are different facts. A session may retain an active objective after restart or fork, but silently starting work when a user opens that session is surprising. The domain needs replayable state without persisted auto-execution authority, and it must remain a plugin on the public agent and session services rather than a special case in the concrete loop.
## Decision

View File

@@ -8,7 +8,7 @@ Status: implemented
长时间运行的目标会跨越单个提示词、轮次或模型请求。若把该目标视为内存中的循环变量,进程重启时就会丢失;若只存放在 UI 状态中,又无法重建模型行为。若把会话中的每个轮次都视为目标进度,与自动工作无关的人类消息也会消耗预算。
持久化的生命周期与继续执行的权限是两个不同事实。会话在重启或 fork(派生)后可以保留活跃目标,但用户打开会话时静默启动工作并不符合直觉。该领域需要可回放的状态,却不能持久化自动执行权限;它还必须作为公共 agent(智能体)与会话 seam 上的插件存在,而不是具体循环中的特例。
持久化的生命周期与继续执行的权限是两个不同事实。会话在重启或 fork(派生)后可以保留活跃目标,但用户打开会话时静默启动工作并不符合直觉。该领域需要可回放的状态,却不能持久化自动执行权限;它还必须作为公共 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 .agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md
2026-07-19-same-session-goal-round-driver.md: a989b9d3487ebbf8ff11a4e34cb08029f1e4ace4
2026-07-19-same-session-goal-round-driver.zh.md: 309b5ce45c2edb516a329f9992cea5558a2aeb8c
2026-07-19-same-session-goal-round-driver.md: 2ca08e4b6f0d16d66663db5275f3bce72bd3a9d7
2026-07-19-same-session-goal-round-driver.zh.md: a41d9705e18cddb851c0a99a8dd7fc901f2be81a

View File

@@ -49,7 +49,7 @@ The driver classifies one closed goal-owned turn as follows:
No abnormal outcome requests an automatic retry. A later human prompt can ask to continue in any language; the model reads the stopped goal and uses the goal tool's resume action, which records a new revision and arms continuation.
### Durability and cancellation seam
### Durability and cancellation contract
Every `goal/changed` notification creates a checkpoint obligation. The driver awaits `ctx.sessions.flush(session)` before reserving work, then checks for a newer mutation, agent lifecycle change, or competing prompt. Turn-end flush failure is reported by the existing `agent/error` notification after `turn/end`; the driver finds that exact closed turn even when a concurrent one-shot injection appended a later turn, associates the failure with the exact attempt, and disarms before the next idle decision.
@@ -75,7 +75,7 @@ The core cancellation test proves notification order and containment: observers
## Alternatives considered
- **Add a goal loop inside `dsh-agent-loop`** — rejected because the public queue, prompt, session, cancellation, and status seams are sufficient, and a concrete-loop branch would privilege one policy.
- **Add a goal loop inside `dsh-agent-loop`** — rejected because the public queue, prompt, session, cancellation, and status contracts are sufficient, and a concrete-loop branch would privilege one policy.
- **Use `agent/turn-continuation` to make every round another step** — rejected because a goal round is an outer policy iteration and must have its own durable user prompt, turn boundary, round count, and failure settlement.
- **Persist a pending reservation** — rejected because a crash cannot prove that queued process memory had reached admission; only the durable `user/message` consumes the round.
- **Retry provider or persistence errors automatically** — rejected because retry policy spends resources and needs explicit authority; stopped phases plus later human resume are simpler and observable.

View File

@@ -49,7 +49,7 @@ Status: implemented
异常结果都不会请求自动重试。之后的人类提示词可以用任何语言要求继续;模型读取已停止目标并调用目标工具的 resume 动作,记录新修订并重新激活继续执行。
### 持久性与取消 seam
### 持久性与取消约定
每次 `goal/changed` 通知都会产生一个检查点义务。驱动器在预留工作前等待 `ctx.sessions.flush(session)`,随后检查是否出现了更新的变更、agent 生命周期变化或竞争提示词。轮次结束时的 flush 失败会在 `turn/end` 之后通过现有 `agent/error` 通知报告;即使并发的一次性注入已追加后续轮次,驱动器仍会找到对应的同一个已关闭轮次,把失败关联到对应的同一次尝试,并在下一次空闲决策前解除激活。
@@ -75,7 +75,7 @@ Status: implemented
## 考虑过的替代方案
- **在 `dsh-agent-loop` 内添加目标循环**——不予采纳,因为公共队列、提示词、会话、取消和状态 seam 已经足够,具体循环分支还会赋予某种策略特权。
- **在 `dsh-agent-loop` 内添加目标循环**——不予采纳,因为公共队列、提示词、会话、取消和状态约定已经足够,具体循环分支还会赋予某种策略特权。
- **使用 `agent/turn-continuation` 把每个 Round 变成另一个步骤**——不予采纳,因为 Goal Round 是外层策略迭代,必须拥有自己的持久用户提示词、轮次边界、Round 计数和失败结算。
- **持久化待处理预留**——不予采纳,因为崩溃无法证明进程内队列已经达到接纳点;只有持久 `user/message` 才消耗回合。
- **自动重试提供方或持久化错误**——不予采纳,因为重试会消耗资源,需要显式授权;停止阶段加之后的人类恢复更简单,也可观察。

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 .agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md
2026-07-20-code-mode-typed-tool-returns.md: 2081d8161f0ee14493a09762b18ec7d9d07ea3c4
2026-07-20-code-mode-typed-tool-returns.zh.md: 808f8f96dbb1655dc00d1ccd9bb7f7ac93c9550f
2026-07-20-code-mode-typed-tool-returns.md: 059c715e35fb856e92d35926e6133701e5602c97
2026-07-20-code-mode-typed-tool-returns.zh.md: 1bb88d29ffd96d65ce06033202499d2780814abd

View File

@@ -51,9 +51,9 @@ declare const tools: {
Before dispatch the bridge snapshots binding arguments as lossless JSON and snapshots the detached value again for an independent durable summary event. Host-side detachment, immutable execution, and output-schema projection all use iterative traversals rather than nested structured clone or recursive freezing. `undefined`, non-finite numbers, `-0`, sparse arrays, cycles, functions, and exotic objects reject that call before the tool runs. Successful dispatch returns `ToolExecutionResult.value`; Native `content`, metadata, and internal error information do not cross to the program.
Code Mode declares its rejection capability on the runtime request as `{ name: "ToolCallError", memberNameProperty: "toolName" }`. The runtime seam treats those names as data: the worker materializes and injects the actual constructor used for `tools` binding failures, so `error instanceof ToolCallError` works without making a generic runtime know about tools. The worker constructs failures and defines their public fields through module-captured error and property-definition intrinsics plus null-prototype descriptors, so model mutations cannot replace the promised rejection with a worker failure. The error has the standard `Error` message plus the exact `toolName`; it deliberately omits `ToolFailure.info`, error codes, and Native content. This is an exception contract for control flow, not a failure union for programmatic classification.
Code Mode declares its rejection capability on the runtime request as `{ name: "ToolCallError", memberNameProperty: "toolName" }`. The runtime Service Definition treats those names as data: the worker materializes and injects the actual constructor used for `tools` binding failures, so `error instanceof ToolCallError` works without making a generic runtime know about tools. The worker constructs failures and defines their public fields through module-captured error and property-definition intrinsics plus null-prototype descriptors, so model mutations cannot replace the promised rejection with a worker failure. The error has the standard `Error` message plus the exact `toolName`; it deliberately omits `ToolFailure.info`, error codes, and Native content. This is an exception contract for control flow, not a failure union for programmatic classification.
Binding arguments and resolutions are revalidated as lossless JSON on both sides of the hostile worker protocol and have no byte cap. Before crossing through structured clone, each detached value is encoded as a flat pre-order token stream whose transport nesting is bounded; the receiver rebuilds it iteratively. Valid application nesting therefore has neither a JavaScript call-stack depth cap nor a platform-specific nested structured-clone limit. At module initialization the worker captures its own realm's `Array.prototype` and `Object.prototype` identities, the native function-source intrinsic used only to recognize foreign-realm plain-container prototypes, and every structural and metering intrinsic used by the JSON boundary. Property writes use null-prototype descriptors, while private array and set operations invoke captured methods without consulting mutable global or prototype slots. Model code can therefore replace helpers such as `Object.keys`, `Array.isArray`, collection methods, string methods, or `Buffer.byteLength`, rewrite intrinsic-prototype constructor slots, or add descriptor-shaped fields to `Object.prototype` without changing validation, wire transport, or byte accounting. The foreign-realm native function-source check still rejects user-authored constructors that imitate `Object` or `Array`. The dependency-light runtime seam names its structural equivalent `CodeJsonValue` so it need not depend on the session-owned canonical type; the generated SDK and tool API use `JsonValue`. Intermediate values are not prompt-truncated, context-spilled, or persisted. This preserves full acquired search, workflow, task, filesystem, and MCP values for programmatic filtering while leaving provider and executor acquisition limits truthful.
Binding arguments and resolutions are revalidated as lossless JSON on both sides of the hostile worker protocol and have no byte cap. Before crossing through structured clone, each detached value is encoded as a flat pre-order token stream whose transport nesting is bounded; the receiver rebuilds it iteratively. Valid application nesting therefore has neither a JavaScript call-stack depth cap nor a platform-specific nested structured-clone limit. At module initialization the worker captures its own realm's `Array.prototype` and `Object.prototype` identities, the native function-source intrinsic used only to recognize foreign-realm plain-container prototypes, and every structural and metering intrinsic used by the JSON boundary. Property writes use null-prototype descriptors, while private array and set operations invoke captured methods without consulting mutable global or prototype slots. Model code can therefore replace helpers such as `Object.keys`, `Array.isArray`, collection methods, string methods, or `Buffer.byteLength`, rewrite intrinsic-prototype constructor slots, or add descriptor-shaped fields to `Object.prototype` without changing validation, wire transport, or byte accounting. The foreign-realm native function-source check still rejects user-authored constructors that imitate `Object` or `Array`. The dependency-light runtime Service Definition names its structural equivalent `CodeJsonValue` so it need not depend on the session-owned canonical type; the generated SDK and tool API use `JsonValue`. Intermediate values are not prompt-truncated, context-spilled, or persisted. This preserves full acquired search, workflow, task, filesystem, and MCP values for programmatic filtering while leaving provider and executor acquisition limits truthful.
### Outer result and output ledger

View File

@@ -51,9 +51,9 @@ declare const tools: {
分发前,桥接层会把绑定参数快照为无损 JSON,再对分离后的值生成一次快照,供独立的持久摘要事件使用。宿主侧的值分离、执行数据的不可变处理与输出 schema 投影均采用迭代遍历,而不使用嵌套结构化克隆或递归冻结。`undefined`、非有限数、`-0`、稀疏数组、循环引用、函数和非普通对象都会使该调用在工具运行前被拒绝。成功分发会返回 `ToolExecutionResult.value`;Native `content`、元数据和内部错误信息不会传入程序。
Code Mode 通过运行时请求中的 `{ name: "ToolCallError", memberNameProperty: "toolName" }` 声明其以异常拒绝 Promise 的能力。运行时 seam 只把这些名称视为数据:worker 会动态生成并注入真正用于 `tools` 绑定失败的构造函数,因此无需让通用运行时了解工具,`error instanceof ToolCallError` 也能成立。worker 使用模块初始化时捕获的 Error 构造函数与属性定义内建方法,配合原型为 null 的属性描述符,构造失败对象并定义其公开字段,因此模型代码的修改不会把约定承诺的 reject 变成 worker 失败。该错误包含标准的 `Error` 消息和确切的 `toolName`,并有意省略 `ToolFailure.info`、错误代码与 Native 内容。这是一项用于控制流的异常约定,而不是供程序分类的失败联合。
Code Mode 通过运行时请求中的 `{ name: "ToolCallError", memberNameProperty: "toolName" }` 声明其以异常拒绝 Promise 的能力。运行时 Service Definition 只把这些名称视为数据:worker 会动态生成并注入真正用于 `tools` 绑定失败的构造函数,因此无需让通用运行时了解工具,`error instanceof ToolCallError` 也能成立。worker 使用模块初始化时捕获的 Error 构造函数与属性定义内建方法,配合原型为 null 的属性描述符,构造失败对象并定义其公开字段,因此模型代码的修改不会把约定承诺的 reject 变成 worker 失败。该错误包含标准的 `Error` 消息和确切的 `toolName`,并有意省略 `ToolFailure.info`、错误代码与 Native 内容。这是一项用于控制流的异常约定,而不是供程序分类的失败联合。
绑定参数与绑定返回值会在不可信 worker 协议的两端重新校验为无损 JSON,且不设字节上限。每个分离后的值在通过结构化克隆跨越边界前,都会编码为扁平的前序 token 流,其传输结构的嵌套深度有界;接收方再以迭代方式重建该值。因此,有效应用数据的嵌套深度既不受 JavaScript 调用栈深度上限限制,也不受特定平台对嵌套结构化克隆施加的上限限制。模块初始化时,worker 会捕获自身 JavaScript 运行域中 `Array.prototype` 和 `Object.prototype` 的引用、仅用于识别其他运行域普通容器原型、可获取原生函数源码的内建函数,以及 JSON 边界用于结构处理和计量的全部内建方法。属性写入使用原型为 null 的属性描述符;内部的数组与集合操作直接调用捕获的方法,不会访问可变的全局或原型槽位。因此,即使模型代码替换 `Object.keys`、`Array.isArray`、集合方法、字符串方法或 `Buffer.byteLength` 等辅助方法,重写内建原型的构造函数槽位,或向 `Object.prototype` 添加形如属性描述符的字段,也不会改变校验、协议传输或字节计量。面向其他运行域的原生函数源码检查仍会拒绝由用户编写、冒充 `Object` 或 `Array` 的构造函数。为保持依赖轻量,运行时 seam 将结构等价类型命名为 `CodeJsonValue`,从而无需依赖会话侧拥有的规范类型;生成的 SDK 和工具 API 则使用 `JsonValue`。这些值不会经过提示词截断、上下文输出落盘或持久化。因此,程序可以完整筛选已经采集的搜索、工作流、任务、文件系统与 MCP 值,同时提供方和执行器的采集上限仍会实际生效。
绑定参数与绑定返回值会在不可信 worker 协议的两端重新校验为无损 JSON,且不设字节上限。每个分离后的值在通过结构化克隆跨越边界前,都会编码为扁平的前序 token 流,其传输结构的嵌套深度有界;接收方再以迭代方式重建该值。因此,有效应用数据的嵌套深度既不受 JavaScript 调用栈深度上限限制,也不受特定平台对嵌套结构化克隆施加的上限限制。模块初始化时,worker 会捕获自身 JavaScript 运行域中 `Array.prototype` 和 `Object.prototype` 的引用、仅用于识别其他运行域普通容器原型、可获取原生函数源码的内建函数,以及 JSON 边界用于结构处理和计量的全部内建方法。属性写入使用原型为 null 的属性描述符;内部的数组与集合操作直接调用捕获的方法,不会访问可变的全局或原型槽位。因此,即使模型代码替换 `Object.keys`、`Array.isArray`、集合方法、字符串方法或 `Buffer.byteLength` 等辅助方法,重写内建原型的构造函数槽位,或向 `Object.prototype` 添加形如属性描述符的字段,也不会改变校验、协议传输或字节计量。面向其他运行域的原生函数源码检查仍会拒绝由用户编写、冒充 `Object` 或 `Array` 的构造函数。为保持依赖轻量,运行时 Service Definition 将结构等价类型命名为 `CodeJsonValue`,从而无需依赖会话侧拥有的规范类型;生成的 SDK 和工具 API 则使用 `JsonValue`。这些值不会经过提示词截断、上下文输出落盘或持久化。因此,程序可以完整筛选已经采集的搜索、工作流、任务、文件系统与 MCP 值,同时提供方和执行器的采集上限仍会实际生效。
### 外层结果与输出账本

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 .agents/notes/implemented/feature/2026-07-21-cross-session-references.md
2026-07-21-cross-session-references.md: b2d428e0937fdd881720720401948a1c3e7ef1f6
2026-07-21-cross-session-references.zh.md: ef89fd76c864c430f7b730d250e6511c2709f97e
2026-07-21-cross-session-references.md: 46424e12949697054da2f920fbe55ca9dcbaf075
2026-07-21-cross-session-references.zh.md: 7956dbef79aa77ab9d2be810420ae8c1b4e82a8d

View File

@@ -6,7 +6,7 @@ English | [中文](2026-07-21-cross-session-references.zh.md)
## Problem
TUI users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, provenance filtering, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
TUI users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, filtering by cited source-event seqs, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
## Decision

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
TUI 用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、来源过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息约定,还会让核心循环绑定某一种 UI 语法。
TUI 用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、按被引用来源事件 seq 过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息约定,还会让核心循环绑定某一种 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 .agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md
2026-07-21-log-backed-session-titles.md: 89e6e67fb9ece4c087ba7b1bc52e61b78b94586c
2026-07-21-log-backed-session-titles.zh.md: 21a0ec5dc9cc90726dda502ba46a52d717c39df5
2026-07-21-log-backed-session-titles.md: 47025ba7e72ea6746d6f4428a81d6d754a8dcd03
2026-07-21-log-backed-session-titles.zh.md: d7e12875354ca1fc66f0035020c7ffe2ddaa9354

View File

@@ -16,7 +16,7 @@ The [`session-title` capability family](../../../../packages/session/README.md)
### Event ownership and folding
Every accepted revision is a log-only `session/title` event. Its payload contains normalized non-empty text, the exact eligible human `user/message` seqs used to derive it, and either fallback provenance or the registered provider id plus optional provider/model route. Before an auxiliary title-model dispatch, the shared helper appends a log-only `session/title-llm-request` event containing the title-provider id, exact source seqs, route, system prompt, messages, and output-token cap; a later generation failure leaves the request auditable. The dispatched envelope is deep-frozen to preserve exact agreement with that record but carries no process-local agent-loop request identity, so loop-only reconstruction checks do not compare it with the main conversation header. Validation failures that never reach dispatch create no request event. `foldSessionTitle()` selects the latest title event and adds that event's seq and timestamp as `SessionTitleSnapshot`. Neither event enters `session.surface` or `deriveMessages()`.
Every accepted revision is a log-only `session/title` event. Its payload contains normalized non-empty text, the exact eligible human `user/message` seqs used to derive it, and either the fallback source kind or the registered provider id plus optional provider/model route. Before an auxiliary title-model dispatch, the shared helper appends a log-only `session/title-llm-request` event containing the title-provider id, exact source seqs, route, system prompt, messages, and output-token cap; a later generation failure leaves the request auditable. The dispatched envelope is deep-frozen to preserve exact agreement with that record but carries no process-local agent-loop request identity, so loop-only reconstruction checks do not compare it with the main conversation header. Validation failures that never reach dispatch create no request event. `foldSessionTitle()` selects the latest title event and adds that event's seq and timestamp as `SessionTitleSnapshot`. Neither event enters `session.surface` or `deriveMessages()`.
The title service appends `session/title` directly after checking its current revision and exact live session; the bundled model helper likewise appends its literal `session/title-llm-request` record before dispatch. Both records may sit between turns without inventing an execution boundary. Persistence admits them to bounded background batches and drains through ordinary checkpoints and lifecycle teardown; title publication does not force a per-event flush. No generic marker, cast, or settlement queue sits between the event owner and `Session.append()`. This is the domain-specific application of the [standalone log-only event decision](../simplification/2026-07-28-remove-synthetic-log-only-turns.md).
@@ -51,8 +51,8 @@ A fork inherits seed title events unchanged, like the rest of its source log —
- **Mutable `SessionHeader` or side metadata** — rejected because it creates a second persistence mutation protocol, weakens immutable identity metadata, makes crash atomicity backend-specific, and gives forks ambiguous copy-versus-reference behavior. The append-only log already owns replayable latest-wins state.
- **Await title generation before returning the agent response** — rejected because auxiliary provider latency and failure would sit on the main interaction's critical path. The deterministic fallback gives immediate useful state while a better title may arrive later.
- **Put titles in derived history or the request prefix** — rejected because UI metadata would consume tokens, change cache identity, and make the main model observe its own label. A log-only event remains reconstructable without becoming model-visible.
- **Permit multiple registered providers and resolve precedence after completion** — rejected because completion order is not product precedence and would make retries, HMR, and provenance nondeterministic. A deployment that needs a composite policy can register one provider that owns that policy.
- **Silently truncate oversized auxiliary input** — rejected because the provider result would claim exact source-message provenance while receiving only partial text. Keeping the prior title and warning preserves truthful attribution.
- **Permit multiple registered providers and resolve precedence after completion** — rejected because completion order is not product precedence and would make retries, HMR, and the recorded provider nondeterministic. A deployment that needs a composite policy can register one provider that owns that policy.
- **Silently truncate oversized auxiliary input** — rejected because the provider result would cite source-message seqs whose complete text it did not receive. Keeping the prior title and warning preserves the exact input record.
- **Index titles in `listSessions()` immediately** — rejected because the existing lightweight metadata list would need per-backend derived-index synchronization. Exact `readTitle()` establishes the read contract without precommitting search or indexing policy.
- **Keep the Web host fallback-only** — rejected because the UI would expose durable titles but never improve them beyond the first-prompt prefix. The first-message provider keeps its latency off the main response path while making model summaries the default Web outcome.

View File

@@ -16,7 +16,7 @@ Status: implemented
### 事件归属与折叠
每个已接受的修订都是纯日志 `session/title` 事件。其载荷包含规范化后的非空文本、用于派生标题的所有合格且来源为人类的 `user/message` 的准确 seq,以及回退来源信息,或已注册的提供方 id 加可选的提供方和模型路由。辅助标题模型发起调用前,共享辅助组件会追加一个纯日志 `session/title-llm-request` 事件,其载荷包含标题提供方 id、准确的源 seq、路由、系统提示词、消息和输出 token 上限;即使后续生成失败,这次请求仍可审计。发送的请求信封经过深度冻结,以确保其与该记录精确一致,但它有意不携带进程本地的 agent loop(智能体循环)请求身份,因此仅针对 agent loop 的重建检查不会将它与主对话请求头进行比较。未进入调用阶段的验证失败不会创建请求事件。`foldSessionTitle()` 选择最新的标题事件,并将该事件的 seq 和时间戳加入 `SessionTitleSnapshot`。这两类事件都不会进入 `session.surface` 或 `deriveMessages()`。
每个已接受的修订都是纯日志 `session/title` 事件。其载荷包含规范化后的非空文本、用于派生标题的所有合格且来源为人类的 `user/message` 的准确 seq,以及回退来源 kind,或已注册的提供方 id 加可选的提供方和模型路由。辅助标题模型发起调用前,共享辅助组件会追加一个纯日志 `session/title-llm-request` 事件,其载荷包含标题提供方 id、准确的源 seq、路由、系统提示词、消息和输出 token 上限;即使后续生成失败,这次请求仍可审计。发送的请求信封经过深度冻结,以确保其与该记录精确一致,但它有意不携带进程本地的 agent loop(智能体循环)请求身份,因此仅针对 agent loop 的重建检查不会将它与主对话请求头进行比较。未进入调用阶段的验证失败不会创建请求事件。`foldSessionTitle()` 选择最新的标题事件,并将该事件的 seq 和时间戳加入 `SessionTitleSnapshot`。这两类事件都不会进入 `session.surface` 或 `deriveMessages()`。
标题服务会在检查当前修订和确切的实时会话后,直接追加 `session/title`;随附模型辅助函数同样会在发起调用前追加其字面量 `session/title-llm-request` 记录。两类记录都可以位于轮次之间,而无需虚构执行边界。持久化会将它们接纳到有界后台批次中,并通过常规检查点和生命周期 teardown 排空;标题发布不会强制逐事件 flush。事件所有方与 `Session.append()` 之间不存在通用标记、类型断言或结算队列。这是[独立纯日志事件决策](../simplification/2026-07-28-remove-synthetic-log-only-turns.md)在特定领域中的应用。
@@ -51,8 +51,8 @@ Status: implemented
- **可变 `SessionHeader` 或独立元数据**:不予采纳,因为这会创建第二套持久化变更协议,削弱不可变身份元数据,让崩溃原子性因后端而异,并使 fork 的复制或引用行为产生歧义。仅追加日志已经负责可回放的后写覆盖状态。
- **返回 agent 响应前等待标题生成**:不予采纳,因为辅助提供方的延迟和故障会进入主交互的关键路径。确定性回退方案可以立即提供可用状态,质量更高的标题则可稍后到达。
- **将标题放入派生历史记录或请求前缀**:不予采纳,因为 UI 元数据会消耗 token、改变缓存标识,并让主模型观察到自己的标签。纯日志事件既保持可重建,又不会变得对模型可见。
- **允许注册多个提供方,并在完成后解析优先级**:不予采纳,因为完成顺序并不等于产品优先级,而且会让重试、HMR 和来源信息变得不确定。需要组合策略的部署可以注册一个自行负责该策略的提供方。
- **静默截断过大的辅助输入**:不予采纳,因为提供方结果会声明准确的源消息来源信息,实际却只接收了部分文本。保留原有标题并发出警告,可以保持归因真实。
- **允许注册多个提供方,并在完成后解析优先级**:不予采纳,因为完成顺序并不等于产品优先级,而且会让重试、HMR 和已记录的提供方变得不确定。需要组合策略的部署可以注册一个自行负责该策略的提供方。
- **静默截断过大的辅助输入**:不予采纳,因为提供方结果会引用源消息 seq,却没有收到这些消息的完整文本。保留原有标题并发出警告,可以保留准确的输入记录。
- **立即在 `listSessions()` 中索引标题**:不予采纳,因为现有的轻量元数据列表将需要逐后端同步派生索引。精确的 `readTitle()` 建立了读取约定,而没有提前锁定搜索或索引策略。
- **让 Web host 只使用回退标题**:不予采纳,因为 UI 虽会显示持久标题,却始终无法将第一条提示词的前缀改进为更好的标题。首消息提供方在主响应路径之外运行,并让模型摘要成为 Web 的默认结果。

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 .agents/notes/implemented/feature/2026-07-24-model-facing-session-query-tools.md
2026-07-24-model-facing-session-query-tools.md: 863f557f11f89ff8dfc121b7da0b653852528394
2026-07-24-model-facing-session-query-tools.zh.md: 584733de7b00f22f186838b058a24c31ff035905
2026-07-24-model-facing-session-query-tools.md: 7dcb25fc4205442bd0b0743a6228427fa66ead1c
2026-07-24-model-facing-session-query-tools.zh.md: c338edcc1f557fae46c745c7aba09698b1d54962

View File

@@ -14,7 +14,7 @@ The unified `ctx.sessionQuery` service exposes exact reads, filters, relationshi
The package entrypoint is only the public composition root for configuration, prompt registration, and tool registration. Its internal modules follow the execution boundary: `input.ts` owns model schemas, normalization, and filter construction; `service-boundary.ts` contains provider calls and model-safe error translation; `workspace-access.ts` owns caller identity, workspace authorization, title access, and lineage projection; `operations.ts` orchestrates the five service workflows; and `presentation.ts` renders tool results and call cards. This keeps policy in its owning layer without changing the package contract.
`session_search` groups full-text matches by session and exposes typed session and event metadata filters. `session_event_search` searches one session, defaulting to the caller's current session. `session_trace` returns the complete authorized ancestor chain and recursive descendant trees. `session_event_trace` returns every known positional replacement and direct provenance relationship for one event. `session_event_read` returns the exact target event as unabridged JSON and optionally summarizes a bounded raw-event window; omitted `before` and `after` values mean target-only.
`session_search` groups full-text matches by session and exposes typed session and event metadata filters. `session_event_search` searches one session, defaulting to the caller's current session. `session_trace` returns the complete authorized ancestor chain and recursive descendant trees. `session_event_trace` returns every known positional replacement and direct cited source-event relationship for one event. `session_event_read` returns the exact target event as unabridged JSON and optionally summarizes a bounded raw-event window; omitted `before` and `after` values mean target-only.
Model-facing filters use flat snake-case fields. Timestamps are timezone-qualified ISO 8601 strings at the tool boundary, convert to inclusive epoch-millisecond ranges for the service, and render as UTC ISO 8601. List values are ORed inside one filter while separate filters are ANDed. Requested parent ids are deduplicated and authority-filtered before FTS, so only parents in the caller workspace enter the provider clause; missing and cross-workspace guesses behave identically, while the root-session marker remains independently ORed into that clause. Event type strings remain open because `SessionEventMap` is merge-extensible; availability and event surface use closed values.

View File

@@ -14,7 +14,7 @@ Status: implemented
该包入口仅作为配置、提示词注册与工具注册的公开组合根。内部模块沿执行边界划分:`input.ts` 负责模型 schema、规范化与过滤条件构造;`service-boundary.ts` 包含提供方调用与面向模型的安全错误转换;`workspace-access.ts` 负责调用者身份、工作区授权、标题访问与谱系投影;`operations.ts` 编排五个服务工作流;`presentation.ts` 渲染工具结果与调用卡片。这样可让策略留在其所属层,同时不改变包约定。
`session_search` 按会话聚合全文匹配,并公开带类型的会话与事件元数据过滤条件。`session_event_search` 搜索一个会话,默认目标为调用者的当前会话。`session_trace` 返回完整的已授权祖先链与递归后代树。`session_event_trace` 返回一个事件所有已知的位置替换关系与直接来源关系。`session_event_read` 以未删节 JSON 返回准确的目标事件,并可选择汇总一个有界的原始事件窗口;省略 `before` 与 `after` 时只返回目标。
`session_search` 按会话聚合全文匹配,并公开带类型的会话与事件元数据过滤条件。`session_event_search` 搜索一个会话,默认目标为调用者的当前会话。`session_trace` 返回完整的已授权祖先链与递归后代树。`session_event_trace` 返回一个事件所有已知的位置替换关系与直接引用来源事件的关系。`session_event_read` 以未删节 JSON 返回准确的目标事件,并可选择汇总一个有界的原始事件窗口;省略 `before` 与 `after` 时只返回目标。
面向模型的过滤条件使用扁平的 snake-case 字段。工具边界上的时间戳采用带时区的 ISO 8601 字符串,转换为服务使用的闭区间毫秒时间戳,并以 UTC ISO 8601 渲染。同一个过滤条件中的列表值按 OR 组合,不同过滤条件按 AND 组合。请求的父会话 id 会在 FTS 之前去重并按权限过滤,因此只有调用者工作区中的父会话会进入提供方条件;缺失与跨工作区的猜测具有相同行为,而根会话标记仍会独立按 OR 加入该条件。由于 `SessionEventMap` 可通过声明合并扩展,事件类型字符串保持开放;可用状态与事件表层使用封闭取值。

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 .agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.md
2026-07-25-workspace-ui-product-flow.md: 7a6a41e19d2930fbcbf7ba5fc9e6809d96e23166
2026-07-25-workspace-ui-product-flow.zh.md: 14f33b0eebcf8885670e72cfb6e2c9a87b7a3432
2026-07-25-workspace-ui-product-flow.md: bfdde9797433eca67e47da73468cba855f2a02b9
2026-07-25-workspace-ui-product-flow.zh.md: 74a9dd6f70396201a78f3204bfdc7d8b5d4f5d98

View File

@@ -82,7 +82,7 @@ Deleting a Workspace registration removes its group without deleting or closing
React components only consume `useSessions`, `useWorkspaces`, and session-scoped hooks; they do not own entity lifecycles. The Zustand store retains only layout, the current view, composer text for ordinary real Sessions, and other purely presentational state. Session and Workspace Intents, materialization phases, errors, and retained prompts reside in the React-free runtime object layer.
The Sidebar and conversation empty hero receive standardized actions through slots: `startSession`, `updateSessionPrompt`, `sendSession`, `open`, and `toggleSidebar`. The Workspace picker reuses the same component and the `createWorkspace` seam; its owner supplies only popover state, an anchor, and a selection callback. The presentation layer does not send `host/workspace-changed` directly; Host events originate only from Host mutations and the stream adapter.
The Sidebar and conversation empty hero receive standardized actions through slots: `startSession`, `updateSessionPrompt`, `sendSession`, `open`, and `toggleSidebar`. The Workspace picker reuses the same component and the `createWorkspace` action; its owner supplies only popover state, an anchor, and a selection callback. The presentation layer does not send `host/workspace-changed` directly; Host events originate only from Host mutations and the stream adapter.
## Alternatives considered

View File

@@ -82,7 +82,7 @@ Workspace 组严格使用 Host 返回的持久顺序。Bootstrap 一次性确定
React 组件只消费 `useSessions`、`useWorkspaces` 与 session-scoped 钩子,不拥有实体生命周期。Zustand store 只保留布局、当前 view、普通真实 Session 的 composer 文本和其他纯呈现状态;Session/Workspace Intent、materialize phase、错误和 retained prompt 位于 React-free 运行时对象层。
Sidebar 与 conversation empty hero 通过 slot 获得标准化动作:`startSession`、`updateSessionPrompt`、`sendSession`、`open` 与 `toggleSidebar`。Workspace picker 复用同一组件与 `createWorkspace` seam;owner 只提供 popover 开关、锚点和选中回调。呈现层不直接发送 `host/workspace-changed`,Host 事件只由 Host mutation 与流适配器产生。
Sidebar 与 conversation empty hero 通过 slot 获得标准化动作:`startSession`、`updateSessionPrompt`、`sendSession`、`open` 与 `toggleSidebar`。Workspace picker 复用同一组件与 `createWorkspace` 动作;owner 只提供 popover 开关、锚点和选中回调。呈现层不直接发送 `host/workspace-changed`,Host 事件只由 Host mutation 与流适配器产生。
## Alternatives considered

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 .agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md
2026-07-26-code-dispatch-log-spill.md: eee8fb73b3f1ddba0a2da3ad5a9d2d4417d5951c
2026-07-26-code-dispatch-log-spill.zh.md: 3ef95b5bc0aec1238fd9bb89f545b7f23938ea79
2026-07-26-code-dispatch-log-spill.md: 3892a4a5a21c03d97d1873c68e2228119010e5c5
2026-07-26-code-dispatch-log-spill.zh.md: c98b29e491e059fe27eb5b1ccea1b9664dce4639

View File

@@ -14,8 +14,8 @@ Since the full-content dispatch logging landed, a `run_code` program that reads
**A log-shaping waterfall on the registry, and the spill policy as its first listener.**
- **Seam**: `tools/code-dispatch-log` — a scope-filtered waterfall the bridge runs (via the registry's PRIVATE `shapeDispatchLog` invoker, handed to the bridge as a capability closure in `RunCodeBridgeOptions` — the waterfall is the public seam, the invoker never widens the service surface; contained: a throwing listener falls back to the unshaped content, with total error formatting so a hostile thrown value cannot escape the containment) over each settled sub-dispatch before appending `tool/code-dispatch`. The payload (`CodeDispatchLog`) carries the outer execution, the hoisted `agent` routing key, the sub-call identity, and the default content — the RENDERED result projection a native `tool/result` would carry (the program itself received the structured `value`). Only the durable copy is shapeable; the model sees neither. Shaping runs OFF the program path as tracked side work, but bounded: past `maxParallelSubCalls` pending log tasks the ordered commit lane holds, so a slow spill backend backpressures the run instead of accumulating unbounded pending I/O; run settlement still drains every task inside the open turn.
- **Policy**: `dsh-spill-policy` registers a second arm on the new seam sharing the exact replacement pipeline of its model-facing arm (same `maxInlineBytes` cap, same preview + locator + within-cap invariant, same best-effort fallbacks), with the artifact labeled `dispatch` under the sub-call id. UIs and replay read the full text through the spill artifact exactly as they do for spilled native results, so the native-parity rendering story survives bounding.
- **Extension point**: `tools/code-dispatch-log` — a scope-filtered waterfall the bridge runs (via the registry's PRIVATE `shapeDispatchLog` invoker, handed to the bridge as a capability closure in `RunCodeBridgeOptions` — the waterfall is the public contract, the invoker never widens the service surface; contained: a throwing listener falls back to the unshaped content, with total error formatting so a hostile thrown value cannot escape the containment) over each settled sub-dispatch before appending `tool/code-dispatch`. The payload (`CodeDispatchLog`) carries the outer execution, the hoisted `agent` routing key, the sub-call identity, and the default content — the RENDERED result projection a native `tool/result` would carry (the program itself received the structured `value`). Only the durable copy is shapeable; the model sees neither. Shaping runs OFF the program path as tracked side work, but bounded: past `maxParallelSubCalls` pending log tasks the ordered commit lane holds, so a slow spill backend backpressures the run instead of accumulating unbounded pending I/O; run settlement still drains every task inside the open turn.
- **Policy**: `dsh-spill-policy` registers a second arm on the new extension point sharing the exact replacement pipeline of its model-facing arm (same `maxInlineBytes` cap, same preview + locator + within-cap invariant, same best-effort fallbacks), with the artifact labeled `dispatch` under the sub-call id. UIs and replay read the full text through the spill artifact exactly as they do for spilled native results, so the native-parity rendering story survives bounding.
- **One deliberate asymmetry**: the model-facing arm skips `read` (the `read → spill → read again` loop); the dispatch-log arm bounds `read` sub-calls too — a log copy is not model context, so the loop cannot happen, and `read` is precisely the tool that produces huge logs.
## Alternatives considered

View File

@@ -14,8 +14,8 @@ Status: implemented
**在注册表上增设一个日志整形 waterfall(瀑布式事件),spill 策略作为其第一个监听器。**
- **seam**:`tools/code-dispatch-log`,一个按作用域过滤的 waterfall,由桥接层在追加 `tool/code-dispatch` 之前对每个已结算的子分发运行(经由注册表的私有 `shapeDispatchLog` 调用器——作为能力闭包经 `RunCodeBridgeOptions` 交给桥接层;waterfall 才是公开 seam,调用器绝不扩大服务接口。故障被兜住:监听器抛出异常时回退到未整形的内容,并用可处理任意抛出值的错误格式化,确保恶意抛出值无法逃出兜底)。载荷(`CodeDispatchLog`)携带外层执行、提升出来的 `agent` 路由键、子调用标识与默认内容——即原生 `tool/result` 所载的渲染后结果投影(程序本身收到的是结构化 `value`)。可整形的只有持久副本;模型两者都看不到。整形作为被跟踪的旁路工作在程序路径之外运行,但有界:待处理日志任务超过 `maxParallelSubCalls` 时有序提交通道会暂停,因此慢速 spill 后端会对整个 run 施加背压,而不是无限累积待完成 I/O;run 结算仍会在开放轮次内排空全部任务。
- **策略**:`dsh-spill-policy` 在新 seam 上注册第二个分支,与其面向模型的分支共用一模一样的替换流水线(同样的 `maxInlineBytes` 上限、同样的预览 + 定位符 + 不超上限不变式、同样的尽力而为回退),产物以 `dispatch` 为标签,记在子调用 id 名下。UI 与回放通过 spill 产物读取全文,方式与读取被 spill 的原生结果完全相同,因此与原生同等保真的渲染在施加边界之后依然成立。
- **扩展点**:`tools/code-dispatch-log`,一个按作用域过滤的 waterfall,由桥接层在追加 `tool/code-dispatch` 之前对每个已结算的子分发运行(经由注册表的私有 `shapeDispatchLog` 调用器——作为能力闭包经 `RunCodeBridgeOptions` 交给桥接层;waterfall 才是公开约定,调用器绝不扩大服务接口。故障被兜住:监听器抛出异常时回退到未整形的内容,并用可处理任意抛出值的错误格式化,确保恶意抛出值无法逃出兜底)。载荷(`CodeDispatchLog`)携带外层执行、提升出来的 `agent` 路由键、子调用标识与默认内容——即原生 `tool/result` 所载的渲染后结果投影(程序本身收到的是结构化 `value`)。可整形的只有持久副本;模型两者都看不到。整形作为被跟踪的旁路工作在程序路径之外运行,但有界:待处理日志任务超过 `maxParallelSubCalls` 时有序提交通道会暂停,因此慢速 spill 后端会对整个 run 施加背压,而不是无限累积待完成 I/O;run 结算仍会在开放轮次内排空全部任务。
- **策略**:`dsh-spill-policy` 在新扩展点上注册第二个分支,与其面向模型的分支共用一模一样的替换流水线(同样的 `maxInlineBytes` 上限、同样的预览 + 定位符 + 不超上限不变式、同样的尽力而为回退),产物以 `dispatch` 为标签,记在子调用 id 名下。UI 与回放通过 spill 产物读取全文,方式与读取被 spill 的原生结果完全相同,因此与原生同等保真的渲染在施加边界之后依然成立。
- **一处有意的不对称**:面向模型的分支跳过 `read`(避免 `read → spill → read again` 循环);分发日志分支则连 `read` 子调用也施加边界:日志副本不是模型上下文,该循环因此不可能发生,而 `read` 恰恰是会产生巨大日志的那个工具。
## 曾考虑的替代方案

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 .agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.md
2026-07-26-code-dispatch-ui-foundation.md: a1629c77304bc7ef744f7a09241bcdfc81e461ae
2026-07-26-code-dispatch-ui-foundation.zh.md: 8f40281454e78d3d1baaf0dce3635bdf01d0a658
2026-07-26-code-dispatch-ui-foundation.md: 1714472018a415114efed7ce189a3d82268dd8e3
2026-07-26-code-dispatch-ui-foundation.zh.md: 8ede615bf15b99c5ecf9a8da427b571180921e54

View File

@@ -4,7 +4,7 @@ Status: implemented
English | [中文](2026-07-26-code-dispatch-ui-foundation.zh.md)
> Scope: the host-side contract changes that let a UI render a Code Mode turn with the same fidelity as native tool calls — the first PR of the Code Mode web-UI stack. The [Code Mode foundation](2026-06-15-code-mode.md) owns the transport design; this note owns the model-visible `description` parameter, the full-content `tool/code-dispatch` payload, and the temporary `DSH_TOOLS_MODE` enablement seam for the `dsh` config tree.
> Scope: the host-side contract changes that let a UI render a Code Mode turn with the same fidelity as native tool calls — the first PR of the Code Mode web-UI stack. The [Code Mode foundation](2026-06-15-code-mode.md) owns the transport design; this note owns the model-visible `description` parameter, the full-content `tool/code-dispatch` payload, and the temporary `DSH_TOOLS_MODE` enablement switch for the `dsh` config tree.
## Problem
@@ -16,7 +16,7 @@ Three changes, one per obstacle:
1. **`run_code` gains a required `description` parameter** (bash's exact contract: active voice, 5-10 words, shown in the UI; whitespace-only rejected at execute). `presentCall` now titles the card with the description and moves the program to `rawInput`. The prompt-side cost is a few tokens per call; the return is that every surface — TUI card, ACP title, web row — gets a human-readable label without parsing TypeScript.
2. **`tool/code-dispatch` logs the sub-call's complete model-facing outcome** — `content: ContentBlock[]` + `isError`, the `tool/result` vocabulary — replacing `resultSummary` and deleting the summarize/cwd-normalization machinery outright. A UI renders a sub-call through the identical code path as a native result, including error text and non-text blocks. The event stays log-only (`deriveMessages()` ignores it): nothing about model context changes.
3. **`DSH_TOOLS_MODE` env var on the `dsh` config tree** (`native`|`code`|`both`; unset keeps the schema default): the `tools` row reads it via `!!js`, and the worker code runtime is mounted unconditionally (Loader metadata is static, so no conditional row exists; a native boot only registers the service — workers spawn per run). This is an explicitly temporary seam: per-session tool-mode selection owned by the web UI is the design goal, and the env var dies when that lands.
3. **`DSH_TOOLS_MODE` env var on the `dsh` config tree** (`native`|`code`|`both`; unset keeps the schema default): the `tools` row reads it via `!!js`, and the worker code runtime is mounted unconditionally (Loader metadata is static, so no conditional row exists; a native boot only registers the service — workers spawn per run). This is an explicitly temporary configuration hook: per-session tool-mode selection owned by the web UI is the design goal, and the env var dies when that lands.
## Alternatives considered

View File

@@ -4,7 +4,7 @@ Status: implemented
[English](2026-07-26-code-dispatch-ui-foundation.md) | 中文
> 范围:让 UI 能以与原生工具调用相同的保真度渲染 Code Mode 轮次的宿主侧约定变更,即 Code Mode Web UI 堆叠 PR(Pull Request)链的第一个 PR。传输设计归 [Code Mode 基础](2026-06-15-code-mode.md)所有;模型可见的 `description` 参数、携带完整内容的 `tool/code-dispatch` 载荷,以及 `dsh` 配置树上临时的 `DSH_TOOLS_MODE` 启用 seam,归本篇所有。
> 范围:让 UI 能以与原生工具调用相同的保真度渲染 Code Mode 轮次的宿主侧约定变更,即 Code Mode Web UI 堆叠 PR(Pull Request)链的第一个 PR。传输设计归 [Code Mode 基础](2026-06-15-code-mode.md)所有;模型可见的 `description` 参数、携带完整内容的 `tool/code-dispatch` 载荷,以及 `dsh` 配置树上临时的 `DSH_TOOLS_MODE` 启用开关,归本篇所有。
## 问题
@@ -16,7 +16,7 @@ Status: implemented
1. **`run_code` 新增必填的 `description` 参数**(与 bash 完全相同的约定:主动语态、5-10 个词、展示在 UI 中;仅含空白的取值在执行时被拒绝)。`presentCall` 现在以该 description 作为卡片标题,并把程序文本移入 `rawInput`。提示词侧的成本是每次调用多出几个 token;换来的是每个表面——TUI 卡片、ACP(Agent Client Protocol)标题、Web 行——都无需解析 TypeScript 就能获得可供人阅读的标签。
2. **`tool/code-dispatch` 记录子调用面向模型的完整结果**(`content: ContentBlock[]` 加 `isError`,即 `tool/result` 的词汇),取代 `resultSummary`,并把摘要与 cwd 归一化机制彻底删除。UI 渲染子调用走的代码路径与渲染原生结果完全相同,包括错误文本和非文本块。该事件保持仅日志(`deriveMessages()` 忽略它):模型上下文没有任何变化。
3. **`dsh` 配置树上的 `DSH_TOOLS_MODE` 环境变量**(`native`|`code`|`both`;未设置时保持 schema 默认值):`tools` 行通过 `!!js` 读取它,worker 代码运行时则无条件挂载(loader 元数据是静态的,因此不存在条件行;native 启动只是注册该服务,worker 要到每次运行时才 spawn)。这是一个明确标注为临时的 seam:设计目标是让 Web UI 拥有按会话的工具模式选择,该目标落地后,这个环境变量随即退役。
3. **`dsh` 配置树上的 `DSH_TOOLS_MODE` 环境变量**(`native`|`code`|`both`;未设置时保持 schema 默认值):`tools` 行通过 `!!js` 读取它,worker 代码运行时则无条件挂载(loader 元数据是静态的,因此不存在条件行;native 启动只是注册该服务,worker 要到每次运行时才 spawn)。这是一个明确标注为临时的配置钩子:设计目标是让 Web 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 .agents/notes/implemented/feature/2026-07-27-tmux-location-context.md
2026-07-27-tmux-location-context.md: dc9e559d1ed5cbb419901303a81e69cb0d0ba828
2026-07-27-tmux-location-context.zh.md: 9c8a56f12406ff17ce9c25538dcfdf8e7aa06f91
2026-07-27-tmux-location-context.md: 2a2188c920b678565173d28fb18268f951cc9a11
2026-07-27-tmux-location-context.zh.md: c0f6e9f5ca56bef9794f55e72ffbbe3ee1be7240

View File

@@ -52,7 +52,7 @@ Unit tests pin: first-step injection and source/surface metadata; the `$TMUX_PAN
- **Push from a tmux hook / background watcher** — rejected: requires installing hooks in the user's tmux and a background process with teardown, to gain mid-step freshness that per-turn context does not need.
- **Run every step** — rejected: location is stable within a turn; re-querying adds token cost without new information. Gating on `step === 1` yields per-turn readings.
- **Raw `child_process`** — rejected: bypasses the sandbox/policy seam and hand-rolls subprocess code the `ctx.bash` executor already owns.
- **Raw `child_process`** — rejected: bypasses the sandbox policy path and hand-rolls subprocess code the `ctx.bash` executor already owns.
- **Include pane/window pixel sizes** — rejected: sizes churn on every resize and add noise; the layout tree already conveys structure.
- **Scrape sibling panes with `capture-pane`** — rejected: large, noisy, and privacy-sensitive; out of scope for "own location".
- **Dynamic system-prompt section** — rejected: replacing a value erases the earlier readings behind prior reasoning and is not reconstructable; one durable attributed message records each location where it became visible.

View File

@@ -52,7 +52,7 @@ window active=<0|1>, pane active=<0|1>, layout <window-layout>
- **由 tmux hook / 后台监视器推送**——否决:需要在用户的 tmux 中安装 hook,并引入带清理的后台进程,只为换取按轮次上下文并不需要的步内新鲜度。
- **每个 step 都运行**——否决:位置在一轮内稳定;重复查询只增加 token 成本而无新信息。以 `step === 1` 为门槛得到按轮次读数。
- **裸 `child_process`**——否决:绕过沙箱/策略 seam,并手写 `ctx.bash` 执行器已拥有的子进程代码。
- **裸 `child_process`**——否决:绕过沙箱策略路径,并手写 `ctx.bash` 执行器已拥有的子进程代码。
- **包含 pane/window 像素尺寸**——否决:尺寸每次缩放都变动、徒增噪声;布局树已传达结构。
- **用 `capture-pane` 抓取相邻 pane**——否决:庞大、嘈杂且涉及隐私;超出"自身位置"范围。
- **动态系统提示区块**——否决:替换某个值会抹去支撑先前推理的历史读数且不可重建;单条持久且带来源的消息在每个位置变得可见时予以记录。

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 .agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md
2026-07-27-trajectory-inspection-ledger.md: e3fc22234c1df449c99eac90af27de7da0b6f202
2026-07-27-trajectory-inspection-ledger.zh.md: 736523ebb02b8ea3c6e4cdbf9bc27f67779c7426
2026-07-27-trajectory-inspection-ledger.md: a905e65942365c17b7513028b275288c82428221
2026-07-27-trajectory-inspection-ledger.zh.md: a8dcfa97a89f3adc6ab540f3d6cc5020cbefb53f

View File

@@ -19,7 +19,7 @@ Trajectory has to make prose, machine payloads, token usage, timing, and nested
- The client runtime exposes a read-only history source independent from Session and SessionManager. Each activated source owns its raw entries, paging, live gap repair, and reconnect rebuild; the ordinary conversation snapshot remains the folded Chat projection. Trajectory opens the source's tail while mounted and requests one older page when the user reaches the loaded range's top, then lazily derives event order, context lineage, schema index, and Requests instead of imposing those structures on every conversation consumer.
- Ordinary generation and compaction calls form one chronological Request projection, distinguished by purpose rather than separate collections. Effective prompt state and its change ride the Request that introduced them; compaction and prompt changes are not independent inspection entities. Request numbering and cumulative usage cover the loaded history window and expand as older pages arrive.
- Call schemas come from the active recorded Request header. Keyless snapshot fixtures deliberately replace that catalog with the non-array `{{tools}}` token, which the durable inspection boundary treats as unavailable instead of attempting to project or fabricate schemas.
- Selecting a record or Request opens an inspector inside Trajectory. Tabs and Summary sections follow the selected entity: Markdown messages expose rendered, source, provenance, and hierarchy views; tools add JSON payload/result and schema views; Requests add options, usage, timing, and result navigation. Scrollable Summary regions keep their scrollbar thumbs transparent until hover or `focus-within`, while retaining the scrollbar reservation and scroll behavior. Images render as media rather than serialized data.
- Selecting a record or Request opens an inspector inside Trajectory. Tabs and Summary sections follow the selected entity: Markdown messages expose rendered content, source fields, provider/model fields, and hierarchy views; tools add JSON payload/result and schema views; Requests add options, usage, timing, and result navigation. Scrollable Summary regions keep their scrollbar thumbs transparent until hover or `focus-within`, while retaining the scrollbar reservation and scroll behavior. Images render as media rather than serialized data.
- Turn folding removes all rows after its first record and replaces them with a compact step/tool-call count; Assistant folding applies the same interaction to its tool-call descendants. Global controls fold or expand both levels.
- A long ledger initially positions the loaded tail at the bottom and mounts only the viewport's row window plus bounded overscan. Request-only separators join the next measurable virtual item, with a terminal separator retaining its own fixed clearance, so the virtualizer never owns a zero-height item. Semantic DOM-safe row keys and ARIA indexes expose identity independently from mount position. A tail with known older history virtualizes immediately even when its loaded projection is below the ordinary row threshold. Stable-key virtualizer anchoring preserves the visible item across prepends and appends; the manual scroll-height fallback applies only when completing pagination disables virtualization. Selection, timeline focus, folding, search, and bottom following address records by stable event or tool-call identity rather than requiring their DOM rows to exist. An explicit loading row covers records until initial positioning finishes and while an older page is pending. The raw window base sequence detects a prepend even when a page adds no surface-visible node.
- The separate Waterfall tab is removed. A fixed Overview above the ledger projects every loaded record with known `startedAt` onto three semantic timing lanes using its own duration. While an older prefix remains unloaded and the viewport includes the loaded domain's start, a neutral ellipsis control covers the truncated edge and loads one earlier page without assigning unknown history a fabricated duration; hovering that control suppresses the ordinary timeline cursor. Finalized Assistant spans divide the recorded interval at the first non-empty token delta, so distinct TTFT and decoding colors retain their actual ratio; incomplete timing falls back to one Assistant color. Hovering for 500 ms exposes exact start/end, total duration, TTFT, and decoding time without relying on the browser's native tooltip delay. Dragging left or right commits an inclusive interval filter: any record whose active interval overlaps either boundary remains visible, records without known timing leave the focused ledger, and clearing the selection restores the full branch. Wheel gestures zoom the time domain. A right-button click clears the interval selection; dragging instead pans an already zoomed viewport without mutating it. The Overview keeps the full time domain while focused so the selection can be resized or cleared without losing orientation.
@@ -53,4 +53,4 @@ Trajectory has to make prose, machine payloads, token usage, timing, and nested
## Consequences
Trajectory shows more useful records per viewport while retaining Turn and Request orientation. Context rewrites and compactions remain inline with their surrounding history, while a rewind begins a successor branch that inherits only the retained prefix. The floating composer leaves the ledger visible to the viewport edge without covering its final rows or hiding horizontal controls. The main ledger omits token usage and duration so content receives the available width; the local inspector exposes those facts together with full payloads, provenance, schemas, and request timing. The Overview uses recorded start/duration and token-boundary facts without fabricating live elapsed time, and its inclusive focus behavior matches the interaction users already know from Chrome DevTools Network. Tail-first paging bounds initial transport and projection work, virtualization bounds mounted row elements, incremental partial projection removes loaded-history length from ordinary token-frame work, and completed-step chunk compaction makes structural rebuilds proportional to inspection-relevant entries rather than the raw token count. Focused component tests pin tail-first paging, prepend anchoring and identity retention, the virtual window, tail following, content-only streaming without repeated scroll writes, streaming structural sharing, high-sequence window folding, timing projection, delayed detail disclosure, folding, record and interval selection, entity-specific tabs, and running/error semantics. A real-browser long-ledger contract pins stable prepend geometry, bounded mounting, top/middle/bottom reachability, and bounded scroll writes across a paced stream; the assembled Web snapshot pins the ledger, Overview timing details, composer overlay geometry, and inspector through the real client composition.
Trajectory shows more useful records per viewport while retaining Turn and Request orientation. Context rewrites and compactions remain inline with their surrounding history, while a rewind begins a successor branch that inherits only the retained prefix. The floating composer leaves the ledger visible to the viewport edge without covering its final rows or hiding horizontal controls. The main ledger omits token usage and duration so content receives the available width; the local inspector exposes those facts together with full payloads, provider/model and source fields, schemas, and request timing. The Overview uses recorded start/duration and token-boundary facts without fabricating live elapsed time, and its inclusive focus behavior matches the interaction users already know from Chrome DevTools Network. Tail-first paging bounds initial transport and projection work, virtualization bounds mounted row elements, incremental partial projection removes loaded-history length from ordinary token-frame work, and completed-step chunk compaction makes structural rebuilds proportional to inspection-relevant entries rather than the raw token count. Focused component tests pin tail-first paging, prepend anchoring and identity retention, the virtual window, tail following, content-only streaming without repeated scroll writes, streaming structural sharing, high-sequence window folding, timing projection, delayed detail disclosure, folding, record and interval selection, entity-specific tabs, and running/error semantics. A real-browser long-ledger contract pins stable prepend geometry, bounded mounting, top/middle/bottom reachability, and bounded scroll writes across a paced stream; the assembled Web snapshot pins the ledger, Overview timing details, composer overlay geometry, and inspector through the real client composition.

View File

@@ -19,8 +19,8 @@ Status: implemented
- 客户端运行时提供独立于 Session 和 SessionManager 的只读历史数据源。每个已激活的数据源自行拥有原始条目、分页、实时缺口修复和重连重建;普通会话快照仍然只是 Chat 所需的折叠投影。Trajectory 在挂载期间打开该数据源的尾部,当用户到达已加载范围顶部时请求一页更早的历史,再按需派生事件顺序、上下文谱系、schema 索引和请求,避免让所有会话消费方承担这些结构。
- 普通生成调用与压缩调用形成一条按时间排序的请求投影,以用途区分而不是放入不同集合。生效的提示词状态及其变化附着在引入它们的请求上;压缩和提示词变化都不是独立检查实体。请求编号和累计用量覆盖已加载的历史窗口,并随更早页面到达而扩展。
- 调用 schema 来自当前生效且已记录的请求头。无密钥快照 fixture(测试前置数据)有意将该目录替换为非数组 token `{{tools}}`,持久化检查边界会将其视为不可用,而不是尝试投影或虚构 schema。
- 选择记录或请求后,轨迹视图内部会打开检查器,其标签页和概述区域随实体类型变化:Markdown 消息提供渲染、源码、来源和层级视图;工具提供 JSON 载荷/结果和 schema 视图;请求提供选项、用量、计时和结果跳转。可滚动的概述区域默认保持滚动条滑块透明,直到悬停或 `focus-within` 时才显示,同时保留滚动条预留空间和滚动行为。图片以媒体形式渲染,而不是显示为序列化数据。
- 折叠轮次时保留其第一条记录,将后续行替换为紧凑的步骤数和工具调用数;折叠助手时对其工具调用后代应用相同交互。全局控件可同时折叠或展开这两个层级。
- 选择记录或请求后,Trajectory 内部会打开检查器,其标签页和概述区域随实体类型变化:Markdown 消息提供渲染内容、来源字段、提供方/模型字段和层级视图;工具提供 JSON 载荷/结果和 schema 视图;请求提供选项、用量、计时和结果跳转。可滚动的概述区域默认保持滚动条滑块透明,直到悬停或 `focus-within` 时才显示,同时保留滚动条预留空间和滚动行为。图片以媒体形式渲染,而不是显示为序列化数据。
- 折叠轮次时保留其第一条记录,并用紧凑的步骤数和工具调用数替换后续所有行;折叠助手时对其工具调用后代应用相同操作。全局控件会折叠或展开这两个层级。
- 长记录表初始时将已加载尾部置于底部,只挂载视口对应的行窗口及有界的额外缓冲行。仅含请求的分隔行并入下一个具备可测高度的虚拟项,末尾分隔行则保留固定留白,因此虚拟化器不会管理零高度项。可安全用于 DOM 的语义行键与 ARIA 索引使标识不依赖挂载位置。只要已知尾部之前仍有更早历史,即使当前已加载投影低于常规行数阈值,也会立即启用虚拟化。基于稳定键的虚拟化器锚定会在向前补页和尾部追加时保留当前可见项;只有分页完成导致虚拟化停用时,才使用手动滚动高度兜底。选择、时间线聚焦、折叠、搜索和末尾跟随均按稳定的事件或工具调用标识定位,不要求对应 DOM 行已存在。初始定位完成前以及更早页面仍在等待时,明确的加载行会遮住真实记录。原始窗口的基准序号即使在一页未增加任何 surface 可见节点时,也能检测到这次向前补页。
- 移除独立的 waterfall(瀑布式事件)标签页。固定在记录表上方的 Overview 区域将所有 `startedAt` 已知的已加载记录按各自耗时投影到三条语义计时轨道。仍有更早前缀尚未加载且 viewport 包含已加载时间域起点时,中性的省略号控件会遮住截断边缘并加载一页更早历史,而不会为未知历史虚构耗时;悬停在该控件上会隐藏普通的时间线光标。已完成的助手时间条以首个非空 token 增量为分界,用不同颜色按真实比例表示 TTFT 与解码时间;计时不完整时退化为单一助手色。悬停 500 ms 后会显示精确起止时刻、总耗时、TTFT 和解码时间,而不依赖浏览器原生 tooltip 的延迟。向左或向右拖动会提交包含边界的区间筛选:任何活动区间与所选区间任一边界重叠的记录都会保留,计时未知的记录会从聚焦后的记录表中移除,清除选择则恢复完整分支。滚轮手势用于缩放时间域。右键单击会清除区间选择;右键拖动则只会平移已放大的 viewport,不会改变该选区。聚焦后,Overview 区域仍保留完整时间范围,以便在不失去方位的情况下调整或清除选择。
- 实时历史更新仅在用户已经跟随记录表末尾时保留底部位置。向上滚动会清除跟随状态,因此流式分块和新追加的记录不会打断对旧记录的检查。末尾跟随与虚拟化器测量仅响应行键和高度,而非内容标识,因此仅含文本的流式帧既不会丢弃测量缓存,也不会重复执行 DOM 滚动写入。
@@ -53,4 +53,4 @@ Status: implemented
## 后果
轨迹视图在保留轮次与请求定位的同时,每个视口可以显示更多有效记录。上下文 `rewrite` 与压缩保持在周边历史中的原始位置,`rewind` 则建立仅继承保留前缀的后继分支。浮动 composer 让记录表一直显示到视口边缘,同时不会遮住最后几行,也不会隐藏横向控件。主记录表省略 token 用量和耗时,让内容获得可用宽度;局部检查器展示这些数据以及完整载荷、来源、schema 和请求计时。Overview 区域使用记录的开始时间、耗时与 token 边界数据,而不虚构实时流逝时间,其包含边界的聚焦行为与用户熟悉的 Chrome DevTools Network 交互一致。尾部优先分页限制初始传输和投影工作量,虚拟化限制已挂载的行元素数量,未完成部分的增量投影让普通 token 帧的工作量不再随已加载历史长度增长,而已完成步骤的分片压缩则让结构重建的工作量与检查所需条目数量成正比,而非与原始 token 数量成正比。针对性组件测试锁定尾部优先分页、向前补页锚定与标识保持、虚拟窗口、末尾跟随、仅含内容的流式输出不会重复写入滚动位置、流式输出的结构共享、高序号窗口折叠、计时投影、延迟展示详情、折叠、记录与区间选择、实体特定标签页和运行/错误语义。真实浏览器中的长记录表约定锁定向前补页时稳定的几何位置、有界挂载、顶部/中部/底部可达性,以及按节奏进行的流式输出中有界的滚动写入;组装后的 Web 快照则通过真实客户端组合锁定记录表、Overview 计时详情、composer 浮层几何形状与检查器。
轨迹视图在保留轮次与请求定位的同时,每个视口可以显示更多有效记录。上下文 `rewrite` 与压缩保持在周边历史中的原始位置,`rewind` 则建立仅继承保留前缀的后继分支。浮动 composer 让记录表一直显示到视口边缘,同时不会遮住最后几行,也不会隐藏横向控件。主记录表省略 token 用量和耗时,让内容获得可用宽度;局部检查器展示这些数据以及完整载荷、提供方/模型字段、来源字段、schema 和请求计时。Overview 区域使用记录的开始时间、耗时与 token 边界数据,而不虚构实时流逝时间,其包含边界的聚焦行为与用户熟悉的 Chrome DevTools Network 交互一致。尾部优先分页限制初始传输和投影工作量,虚拟化限制已挂载的行元素数量,未完成部分的增量投影让普通 token 帧的工作量不再随已加载历史长度增长,而已完成步骤的分片压缩则让结构重建的工作量与检查所需条目数量成正比,而非与原始 token 数量成正比。针对性组件测试锁定尾部优先分页、向前补页锚定与标识保持、虚拟窗口、末尾跟随、仅含内容的流式输出不会重复写入滚动位置、流式输出的结构共享、高序号窗口折叠、计时投影、延迟展示详情、折叠、记录与区间选择、实体特定标签页和运行/错误语义。真实浏览器中的长记录表约定锁定向前补页时稳定的几何位置、有界挂载、顶部/中部/底部可达性,以及按节奏进行的流式输出中有界的滚动写入;组装后的 Web 快照则通过真实客户端组合锁定记录表、Overview 计时详情、composer 浮层几何形状与检查器。

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 .agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: c5c6c4eac053e9a3adfa2a2a441ac492ba3391ac
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: fe0e93cd13d0decb8d6a95abb0c5feb141dcb78b
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: e4ceeb6b1bc0b9e10fb5383454cbac1b404d4ad8
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 18e5c891f5810135b1ed5e6cfc27b94ad452c575

View File

@@ -10,7 +10,7 @@ The stdio JSON-RPC serving surface (`@deepseek-ai/dsh-jsonrpc`, the [single-exe
## Decision
Three packages, layered exactly like the existing Python stack, plus one seam registration:
Three packages, layered exactly like the existing Python stack, plus one Service provider registration:
- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/scaffold/protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-jsonrpc` (which now imports it), and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. The package root explicitly exports that complete interface and provides no source-module deep imports. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. One behavioral change: an error response now rejects with `JsonRpcResponseError` carrying the wire `code`/`data` (the Python client already preserved these; the old transport threw a bare `Error` with only the message).
- **`@deepseek-ai/dsh-sdk-client`** (`packages/scaffold/client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Its package-root consumer interface explicitly exports both client layers, caller-facing types, and the protocol-owned `JsonRpcResponseError`; source modules, normalization helpers, and the notification producer stay internal. `TurnResult.events` contains only the root session's typed events, while `notifications` retains session ids across the root and descendants discovered from `subagent.started`; session-tree scoping is client-side, mirroring `client.py`. Deliberate asymmetries with Python: the launch spec is explicit `command`/`args` (no bundled-runtime resolution — that is a distribution concern with no TS consumer yet); `env` replaces rather than merges (callers own credential policy; `scrubbedParentEnv` from the subprocess seam is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit (the client runs outside any harness context, so it cannot ride `ctx.subprocess`).
@@ -30,7 +30,7 @@ Four tiers, per [testing policy](../../../../docs/testing.md):
## Alternatives considered
**Import wire types from `dsh-jsonrpc` instead of extracting a protocol package.** Makes every SDK consumer (including `subagent-dsh-sdk`, which must not serve JSON-RPC) depend on the server plugin and its `dsh-agent`/`dsh-llm-deepseek` peer set, and leaves the notification payloads anonymous. The capability-seam rule (interface/implementation/consumer as separate packages) already names this shape; the transport is genuinely two-sided.
**Import wire types from `dsh-jsonrpc` instead of extracting a protocol package.** Makes every SDK consumer (including `subagent-dsh-sdk`, which must not serve JSON-RPC) depend on the server plugin and its `dsh-agent`/`dsh-llm-deepseek` peer set, and leaves the notification payloads anonymous. The capability-seam rule (Service Definition / Service provider / Consumer as separate packages) already names this shape; the transport is genuinely two-sided.
**Have `subagent-dsh-sdk` speak raw JSON-RPC without the client SDK.** Duplicates the request/notification pairing, subscription fan-out, timeout, and teardown logic the SDK exists to own; the user's ask was explicitly a backend that *uses* the SDK, and the layering earns its keep by making the backend ~200 lines of policy over a reusable client.
@@ -38,7 +38,7 @@ Four tiers, per [testing policy](../../../../docs/testing.md):
**Give the TS SDK bundled-runtime resolution parity with Python.** Python's carrier resolution exists to ship wheels to users without Node. A TypeScript consumer definitionally has Node and (in-repo) the workspace; inventing a distribution story with no consumer violates the require-current-need rule. Deferred until a real npm-distribution consumer appears.
**Export source modules, normalization helpers, and subscription producer operations.** These are implementation seams with no caller need; exposing them would make callers learn how the client validates and distributes wire input. The package roots instead enumerate the supported client and protocol interfaces, and the client re-exports the one protocol error callers must distinguish.
**Export source modules, normalization helpers, and subscription producer operations.** These are implementation details with no caller need; exposing them would make callers learn how the client validates and distributes wire input. The package roots instead enumerate the supported client and protocol interfaces, and the client re-exports the one protocol error callers must distinguish.
**Reuse `dsh-acp-snapshot`'s `runScenario` for the SDK snapshots.** That harness speaks ACP (`ClientSideConnection`, `InputStep` scripts). The SDK suite's whole point is to drive the *SDK client* as the entry surface; it reuses the normalize/refresh library layer (`normalizeSessionLog`, `refreshFixtureReplacements`, …) and leaves the ACP driver alone.

View File

@@ -10,7 +10,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-jsonrpc`,见[单文件
## 决策
三个包,分层与既有 Python 栈完全一致,外加一个 seam 注册:
三个包,分层与既有 Python 栈完全一致,外加一个 Service provider 注册:
- **`@deepseek-ai/dsh-sdk-protocol`**(`packages/scaffold/protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport` 从 `dsh-jsonrpc` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result`、`SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。该包根显式导出这一完整接口,且不提供指向源模块的深层导入。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data` 的 `JsonRpcResponseError` 拒绝(Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。
- **`@deepseek-ai/dsh-sdk-client`**(`packages/scaffold/client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(spawn、分帧、通知扇出、有类型的错误表面、经共享 dispose(资源释放)阶梯关闭至完全停稳)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize`、`run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。其包根消费方接口显式导出两层客户端、面向调用方的类型,以及协议包所拥有的 `JsonRpcResponseError`;源模块、规范化辅助函数和通知投递端都保留为内部实现。`TurnResult.events` 只包含根会话的类型化事件,而 `notifications` 则保留根会话及从 `subagent.started` 发现的后代各自的会话 id;基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费方的发行问题);`env` 整体替换而非合并(凭据策略归调用方;subprocess seam 的 `scrubbedParentEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`(Python 只暴露 `status`);拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出(客户端运行在任何 harness 上下文之外,无法搭乘 `ctx.subprocess`)。
@@ -30,7 +30,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-jsonrpc`,见[单文件
## 考虑过的替代方案
**从 `dsh-jsonrpc` 导入线类型而不是提取协议包。** 会让每个 SDK 消费方(包括绝不能提供 JSON-RPC 服务的 `subagent-dsh-sdk`)依赖服务器插件及其 `dsh-agent`/`dsh-llm-deepseek` peer 集合,且通知载荷仍然匿名。能力 seam 规则(接口/实现/消费方三个包分立)已经点名了这种形态;这个传输是货真价实的双边物。
**从 `dsh-jsonrpc` 导入线类型而不是提取协议包。** 会让每个 SDK 消费方(包括绝不能提供 JSON-RPC 服务的 `subagent-dsh-sdk`)依赖服务器插件及其 `dsh-agent`/`dsh-llm-deepseek` peer 集合,且通知载荷仍然匿名。能力 seam 规则(Service Definition/Service provider/Consumer 三个包分立)已经点名了这种形态;这个传输是货真价实的双边物。
**让 `subagent-dsh-sdk` 直说裸 JSON-RPC、绕开客户端 SDK。** 会复制 SDK 存在意义所在的请求/通知配对、订阅扇出、超时与拆除逻辑;用户的要求明确是一个*使用* SDK 的后端,分层的回报是后端成为可复用客户端之上约 200 行的纯策略。
@@ -38,7 +38,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-jsonrpc`,见[单文件
**给 TS SDK 与 Python 对等的捆绑运行时解析。** Python 的载体解析是为了给没有 Node 的用户发 wheel。TypeScript 消费方按定义就有 Node,且仓库内消费方还有工作区;为尚不存在的消费方编造发行方案违反「只实现当前需求」的规则。推迟到真实的 npm 发行消费方出现时再处理。
**导出源模块、规范化辅助函数和订阅投递端操作。** 这些都是调用方不需要的实现 seam;暴露它们会让调用方不得不理解客户端如何校验与分发线输入。各包根转而枚举受支持的客户端接口与协议接口,客户端则只重新导出调用方必须区分的那一种协议错误。
**导出源模块、规范化辅助函数和订阅投递端操作。** 这些都是调用方不需要的实现细节;暴露它们会让调用方不得不理解客户端如何校验与分发线输入。各包根转而枚举受支持的客户端接口与协议接口,客户端则只重新导出调用方必须区分的那一种协议错误。
**复用 `dsh-acp-snapshot` 的 `runScenario` 做 SDK 快照。** 那个 harness 说 ACP(`ClientSideConnection`、`InputStep` 脚本)。SDK 套件的全部意义就是以 *SDK 客户端*为入口表面;它复用 normalize/refresh 库层(`normalizeSessionLog`、`refreshFixtureReplacements`……),不动 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 .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
2026-07-27-web-subagent-conversations.md: 46c4949c56ebffe78e5ffda126a5784a1c648421
2026-07-27-web-subagent-conversations.zh.md: f06e5f38ba49d35ac28582f1ad3ff5eebc8ace15
2026-07-27-web-subagent-conversations.md: 42b1a553adc0222d33feeea21fa5690235d6e5e3
2026-07-27-web-subagent-conversations.zh.md: d373662987f7afc820b180b360493c5725f13617

Some files were not shown because too many files have changed in this diff Show More