Merge origin/master into codex/provider-retry-policy

This commit is contained in:
Turtle
2026-07-25 10:35:22 +08:00
766 changed files with 19344 additions and 1424 deletions

View File

@@ -17,8 +17,8 @@ sequenceDiagram
participant Session
participant Persistence
participant SDK as UI or SDK listener
User->>Agent: send(content)
Agent-->>SDK: <code>agent/queued</code>
User->>Agent: followup(content)
Agent-->>SDK: <code>agent/inbox/enqueue</code>
Agent->>Driver: queued work wakes driver
Driver-->>SDK: <code>agent/status</code> running
Driver->>Session: <code>turn/start</code>

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
architecture.md: 3347e529f7e2c7b4c376d01132851d883c90182e
architecture.zh.md: f3d3bf8b19bace2124b89c3b6ee7e4587445b9fe
architecture.md: 33cd996baeab0b52499a8617d2013ea80271321d
architecture.zh.md: 1a2256633f63ed66404bfbca3696aa0c32c763aa

View File

@@ -6,7 +6,7 @@ English | [中文](architecture.zh.md)
## Overview
Harnesses are [Cordis](cordis-primer.md) contexts whose packages contribute services, typed events, and disposable registrations.
Harnesses are [Cordis](cordis-primer.md) contexts with package-contributed services, typed events, and disposable registrations.
`packages/core/` groups the default agent flow; capabilities remain plugins.
@@ -49,7 +49,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts whose packages contribute serv
## Event
Events form the service extension API; see the exhaustive [events catalog](cordis-catalog/events.md) and [producer/consumer map](event-producer-consumer.md).
Events form the service extension API; see the [catalog](cordis-catalog/events.md) and [producer/consumer map](event-producer-consumer.md).
### Event Domains
@@ -63,11 +63,11 @@ Waterfall events behave like around-middleware: a listener delegates by calling
## Default Loop Lifecycle
The shipped loop runs prompt-to-checkpoint work through plugin services and events.
The loop runs through plugin services and events.
A **session** is append-only. Each ordinary **turn** claims one queued `send()` item; injection claims none. A successor awaits the preceding claimed turn's checkpoint but may share its `running` interval ([decision](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)). A turn ends when model and plugins stop it; a **step** is one model request plus tools. In the [sequence below](agent-lifecycle.md), quotes mark durable events.
A **session** is append-only. Each ordinary **turn** claims one queued message; injection claims none. Successors await the preceding checkpoint but may share its `running` interval ([decision](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)). A **step** is one model request plus tools; quotes in the [sequence below](agent-lifecycle.md) mark durable events.
Creation without an id mints `<config-id>-session-<uuid>`; `sessionId` restores-or-creates, while `resumeSessionId` requires history. Resume restores lineage and delegation depth before publication. Startup failures emit `agent-loop/config-start-failed`; teardown is otherwise silent.
Creation without an id mints `<config-id>-session-<uuid>`; `sessionId` resumes or creates, while `resumeSessionId` requires history. Resume restores lineage and delegation depth before publication. Setup failures emit `agent-loop/config-start-failed`; teardown is silent.
### Turn Flow
@@ -115,39 +115,39 @@ forever:
checkpoint persistence and notify idle/running status
```
Each step assembles ordered prompt sections, tool schemas, and variables; unknown references fail the turn. `dsh-system-prompt` owns identity and persona, while the loop supplies `model` and `cwd` ([prompt ownership](../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)).
Steps assemble ordered prompt sections, tool schemas, and variables; unknown references fail turns. `dsh-system-prompt` owns identity and persona; the loop supplies `model` and `cwd` ([ownership](../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)).
Tool-time context—including async `inject()` and post-tool `additionalContexts`settles after results. Steering drains before `agent/post-step`, which sees durable output, results, context, and steering. Leftovers queue. Terminal `agent/turn-stop` remains authoritative through close/flush; later steering is discarded while queued prompts remain.
Async `inject()` and post-tool `additionalContexts` settle after results; steering drains before `agent/post-step`. Leftovers queue. Terminal `agent/turn-stop` remains authoritative through close/flush and discards later steering, not queued prompts.
Pruning precedes summaries; overflow retries require durable progress. Adapters register nested `retryPolicy`; normal bounds transient failures, while always retries until success or cancellation ([compaction](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md), [retry foundation](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md), [provider policy](../.agents/notes/implemented/feature/2026-07-24-provider-retry-policies.md)).
Pruning precedes summaries; overflow retries require durable progress. Adapters register nested `retryPolicy`; normal bounds transient retries, while always retries until success or cancellation ([compaction](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md), [retry foundation](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md), [provider policy](../.agents/notes/implemented/feature/2026-07-24-provider-retry-policies.md)).
### Failure Boundaries
Adapter failures close the step before `agent/request-error` with exact `Error`, `LlmFailure`, and history. Retry opens another step; success clears history; exhaustion stores failure on `turn/end`. Failed chunks commit no message/tool.
Adapter failures close the step before `agent/request-error` with exact `Error`, `LlmFailure`, and history. Retries open steps; success clears history; exhaustion stores failure on `turn/end`. Failed chunks commit nothing.
Other failures use `agent/error`. Cancellation and disposal beat recovery; undispatched tool calls get synthetic `tool/call`/`ABORTED_BEFORE_DISPATCH` pairs. The turn signal retires before `turn/end`. Effective `cancel()` emits its typed cause before clearing queues and aborting; observers cannot veto, idle calls emit nothing, and durability records `aborted`. Disposal awaits quiescence ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)).
Other failures use `agent/error`. Cancellation and disposal beat recovery; undispatched tools get synthetic `tool/call`/`ABORTED_BEFORE_DISPATCH` pairs. The signal retires before `turn/end`. Effective `cancel()` emits its cause, clears queues, and aborts; observers cannot veto, idle calls emit nothing, and durability records `aborted`. Disposal awaits quiescence ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)).
Session events are turn-enclosed. Reload closes an interrupted tail with a synthetic `interrupted` turn end. Post-close failures report only through `agent/error`; no safe in-turn position remains. Each turn has one `TurnEndReason`; [TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap) owns the variants.
Session events are turn-enclosed; reload closes an interrupted tail with a synthetic `interrupted` turn end. Post-close failures use `agent/error`. Each turn has one [TurnEndReason](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap).
### Agent Handles
`ctx.agents` owns live agents and returns `AgentHandle { agent, dispose() }`. Plugins use `send()`, `steer()`, `inject()`, `cancel()`, and `whenIdle()`. The caller fiber, factory provider, and consumer handle co-own teardown through one awaited disposer.
`ctx.agents` returns `AgentHandle { agent, dispose() }`. Plugins use intent helpers `followup()`, `queue()`, `steer()`, and `inject()`; callers with exact routing facts use mandatory-field `send()` ([decision](../.agents/notes/implemented/architecture/2026-07-24-intent-named-agent-delivery.md)). `cancel()` and `whenIdle()` control lifecycle. Caller, provider, and handle co-own teardown.
### Agent Scope
Each agent owns a scoped `agent.ctx`; shared storage overlays global tool, prompt, and command entries while preserving domain views ([decision](../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md)). Scoped listeners filter dispatch, and every scoped contribution unwinds with awaited cleanup. `CreateAgentOptions.setup(agentCtx)` composes before publication. Typed resolvers derive carrier checks from merged `Events` and `scopeTarget` ([semantic gates](../.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md)). See [agent scope](../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md) and [subagent composition](../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md). `AgentLoop` runs inside `ctx.agents.withInitiator()`; private orchestration derives `agent.session`, while turn, step, signal, cwd, and authority remain explicit ([decision](../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)).
Each agent owns a scoped `agent.ctx` over global tool, prompt, and command storage ([decision](../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md)); scoped listeners filter and contributions unwind with awaited cleanup. `CreateAgentOptions.setup(agentCtx)` composes before publication; typed resolvers derive carrier checks from `Events` and `scopeTarget` ([gates](../.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md)). `AgentLoop` runs inside `ctx.agents.withInitiator()`; private orchestration derives `agent.session`, while turn, step, signal, cwd, and authority stay explicit ([decision](../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)). See [agent scope](../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md) and [subagent composition](../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md).
## State
### Session Log
The session log is authoritative. `deriveMessages()` projects model history; raw `assistant/chunk` events remain for replay and UI fidelity. Fork, resume, transcript rendering, telemetry, and persistence derive from the same stream.
The session log is authoritative. `deriveMessages()` projects model history; raw `assistant/chunk` events preserve replay and UI fidelity. Fork, resume, transcripts, telemetry, and persistence share that stream.
**Model-visible ⟺ logged**: the log reconstructs every request — messages at `step/start` fronted by the header's session prefix, and headers by folding `request/header` — and the package-owned `dsh-agent-loop/invariant` can assert it through `ctx.invariants` ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
**Model-visible ⟺ logged**: `step/start` messages plus the header's session prefix and folded `request/header` reconstruct every request; `dsh-agent-loop/invariant` asserts this through `ctx.invariants` ([decision](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
Durability is a plugin concern. Backends buffer synchronous `session/event` notifications. The semantic checkpoint policy drains requests before adapter dispatch, recorded top-level calls before tool dispatch, and complete response/result batches at `agent/post-step`; the loop retains the final turn-end checkpoint. `SessionPersistence` stores `SessionEvent` directly and metadata in `SessionHeader`; JSONL defaults to checksummed Zstandard, with SQLite under one contract ([decision](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md)).
Durability is a plugin concern; backends buffer synchronous `session/event` notifications. Checkpoints drain before adapter dispatch, recorded top-level tool calls before tool dispatch, complete response/result batches at `agent/post-step`, and final turn ends. `SessionPersistence` stores `SessionEvent` plus `SessionHeader` metadata; JSONL defaults to checksummed Zstandard, with SQLite under one contract ([decision](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md)).
`ctx.sessions.appendOutOfBand()` joins log-only events to an open turn or creates a flushed zero-step turn. `session/title` folds latest-wins with source seqs/provenance; fallback and its optional provider never delay responses. Forks inherit titles ([decision](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)).
`ctx.sessions.appendOutOfBand()` joins plugin-owned log-only events to an open turn or creates a balanced, flushed zero-step turn. `session/title` folds latest-wins with source seqs and provenance; its immediate fallback and sole optional async provider never delay the agent response. Forks inherit titles ([decision](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)).
### Model Content

View File

@@ -49,7 +49,7 @@
## 事件
事件构成服务的扩展 API完整清单见[事件目录](cordis-catalog/events.md)和[生产方与消费方映射](event-producer-consumer.md)。
事件构成服务的扩展 API见[事件目录](cordis-catalog/events.md)和[生产方与消费方映射](event-producer-consumer.md)。
### 事件域
@@ -63,11 +63,11 @@ waterfall瀑布式事件的行为类似环绕中间件监听器调用 `
## 默认循环生命周期
已交付的循环通过插件服务和事件,处理从提示词到检查点的工作
循环通过插件服务和事件运行
**会话**采用仅追加方式。每个普通**轮次**领取一已排队的 `send()` 输入;注入不领取输入。后续轮次会等待前一个已领取轮次的检查点,但可以与共用同一个 `running` 区间([决策](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md))。模型和插件停止轮次时,该轮次结束;一个**步骤**包含一次模型请求及其工具在[下文时序](agent-lifecycle.md)中,引号标记持久事件。
**会话**采用仅追加方式。每个普通**轮次**领取一已排队的消息;注入不领取消息。后续轮次会等待前一个检查点,但可以与前一轮次共用同一个 `running` 区间([决策](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md))。一个**步骤**包含一次模型请求及其工具在[下文时序](agent-lifecycle.md)中,引号标记持久事件。
未提供 id 时会生成 `<config-id>-session-<uuid>``sessionId` 用于恢复或创建,而 `resumeSessionId` 要求已有历史。恢复流程在发布前还原沿袭关系和委托深度。初始化失败会发出 `agent-loop/config-start-failed`其余拆卸过程保持静默。
未提供 id 时,创建流程会生成 `<config-id>-session-<uuid>``sessionId` 用于恢复或创建会话,而 `resumeSessionId` 要求已有历史。恢复流程在发布前还原沿袭关系和委托深度。初始化失败会发出 `agent-loop/config-start-failed`;拆卸过程保持静默。
### 轮次流程
@@ -115,39 +115,39 @@ forever:
checkpoint persistence and notify idle/running status
```
每个步骤会组装有序提示词片段、工具 schema 和变量;未知引用会使轮次失败。`dsh-system-prompt` 负责身份和角色设定循环提供 `model``cwd`[提示词归属](../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md))。
步骤会组装有序提示词片段、工具 schema 和变量;未知引用会使轮次失败。`dsh-system-prompt` 负责身份和角色设定循环提供 `model``cwd`[归属](../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md))。
工具执行阶段的上下文,包括异步 `inject()` 和工具执行后的 `additionalContexts`会在结果产生后稳定steering中途引导会在 `agent/post-step` 前排空;该事件会观察持久输出、结果、上下文和 steering。余留内容进入队列。终止型 `agent/turn-stop` 在关闭和刷写期间始终具有最终决定权后续 steering 会被丢弃排队提示词仍予保留
异步 `inject()` 和工具执行后的 `additionalContexts` 会在结果产生后稳定steering中途引导会在 `agent/post-step` 前排空。余留内容进入队列。终止型 `agent/turn-stop` 在关闭和刷写期间始终具有最终决定权,会丢弃后续 steering,而不丢弃排队提示词。
裁剪先于摘要;溢出重试必须取得持久进展。适配器注册嵌套的 `retryPolicy`normal 限制瞬态错误重试次数always 则持续重试,直至成功或取消([压缩](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)、[重试基础](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md)、[提供方策略](../.agents/notes/implemented/feature/2026-07-24-provider-retry-policies.md))。
裁剪先于摘要;溢出重试必须取得持久进展。适配器注册嵌套的 `retryPolicy`normal 瞬态错误实行有界重试always 则持续重试,直至成功或取消([压缩](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)、[重试基础](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md)、[提供方策略](../.agents/notes/implemented/feature/2026-07-24-provider-retry-policies.md))。
### 失败边界
适配器故障会先关闭步骤,再进入 `agent/request-error`;该事件会收到准确的 `Error``LlmFailure` 和历史记录。重试会开启另一个步骤;成功会清除历史记录;重试耗尽后,故障存入 `turn/end`。失败分片不会提交消息或工具
适配器故障会先关闭步骤,再进入 `agent/request-error`;该事件会收到准确的 `Error``LlmFailure` 和历史记录。重试会开启步骤;成功会清除历史记录;重试耗尽后,故障存入 `turn/end`。失败分片不会提交任何内容
其他故障使用 `agent/error`。取消和资源释放均优先于恢复;尚未分派的工具调用会得到合成的 `tool/call`/`ABORTED_BEFORE_DISPATCH` 对。轮次信号会在 `turn/end` 前失效。实际生效的 `cancel()`清空队列中止前发出类型化原因;观察方不能否决该操作,空闲状态下的调用不发出任何事件,持久化会记录 `aborted`。dispose资源释放会等待系统停稳[决策](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md))。
其他故障使用 `agent/error`。取消和资源释放均优先于恢复;尚未分派的工具会得到合成的 `tool/call`/`ABORTED_BEFORE_DISPATCH` 对。信号会在 `turn/end` 前失效。实际生效的 `cancel()`发出原因、清空队列中止;观察方不能否决该操作,空闲状态下的调用不发出任何事件,持久化会记录 `aborted`。dispose资源释放会等待系统停稳[决策](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md))。
会话事件均位于轮次边界内重新加载会用合成的 `interrupted` 轮次结束事件闭合中断的日志尾部。关闭后的故障只通过 `agent/error` 报告;此时已没有安全的轮次内位置。每个轮次有一个 `TurnEndReason`;各变体由 [TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap) 统一定义
会话事件均位于轮次边界内重新加载会用合成的 `interrupted` 轮次结束事件闭合中断的日志尾部。关闭后的故障使用 `agent/error`。每个轮次有一个 [TurnEndReason](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap)。
### Agent 句柄
`ctx.agents` 拥有活跃 agent返回 `AgentHandle { agent, dispose() }`。插件使用 `send()``steer()``inject()``cancel()``whenIdle()`。调用方 fiber、工厂提供方和消费方句柄通过同一个需等待完成的 disposer 共同拥有拆卸过程。
`ctx.agents` 返回 `AgentHandle { agent, dispose() }`。插件使用按意图命名的辅助方法 `followup()``queue()``steer()``inject()`;持有确切路由信息的调用方使用各字段均为必填项的 `send()`[决策](../.agents/notes/implemented/architecture/2026-07-24-intent-named-agent-delivery.md))。`cancel()``whenIdle()` 控制生命周期。调用方、提供方和句柄共同拥有拆卸过程。
### Agent 作用域
每个 agent 都拥有一个作用域化 `agent.ctx`;共享存储会在全局工具、提示词和命令条目之上叠加作用域条目,同时保留各领域视图[决策](../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md)作用域监听器会过滤分派,每项作用域贡献会在撤销时等待清理完成。`CreateAgentOptions.setup(agentCtx)` 在发布前完成组合类型化解析器从合并后的 `Events``scopeTarget` 推导载体检查([语义门禁](../.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md))。参见 [agent 作用域](../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md)和 [subagent 组合](../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md)。`AgentLoop``ctx.agents.withInitiator()` 内运行;私有编排会派生 `agent.session`而轮次、步骤、信号、cwd 和权限仍保持显式([决策](../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)
每个 agent 都拥有一个作用于全局工具、提示词和命令存储的作用域化 `agent.ctx`[决策](../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md)作用域监听器会过滤分派,各项贡献会在撤销时等待清理完成。`CreateAgentOptions.setup(agentCtx)` 在发布前完成组合类型化解析器从 `Events``scopeTarget` 推导载体检查([门禁](../.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md))。`AgentLoop``ctx.agents.withInitiator()` 内运行;私有编排会派生 `agent.session`而轮次、步骤、信号、cwd 和权限仍保持显式([决策](../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md))。参见 [agent 作用域](../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md)和 [subagent 组合](../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md)。
## 状态
### 会话日志
会话日志是权威依据。`deriveMessages()` 投影出模型历史;原始 `assistant/chunk` 事件留在日志中,以保证回放和 UI 保真。fork、恢复、transcript文本记录渲染、遥测和持久化均派生自同一个事件流。
会话日志是权威依据。`deriveMessages()` 投影出模型历史;原始 `assistant/chunk` 事件留回放和 UI 保真。fork、恢复、transcript文本记录、遥测和持久化共用该事件流。
**模型可见 ⟺ 已记录**日志可以重建每个请求,包括由请求头会话前缀置于开头的 `step/start` 消息,以及通过折叠 `request/header` 得到的请求头;开发期不变量会断言这一点([可重建性](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md))。
**模型可见 ⟺ 已记录**`step/start` 消息、请求头中的会话前缀和折叠后的 `request/header` 共同重建每个请求;`dsh-agent-loop/invariant` 通过 `ctx.invariants` 断言这一点([决策](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md))。
持久性由插件负责后端会缓冲同步的 `session/event` 通知。语义检查点策略会在适配器分发前刷写请求,在工具分发前刷写已记录的顶层调用,`agent/post-step` 刷写完整的响应与结果批次;循环仍保留最终的轮次结束检查点`SessionPersistence` 直接存储 `SessionEvent`,并将元数据存入 `SessionHeader`JSONL 默认采用带校验和的 ZstandardSQLite 则遵循同一契约([决策](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md))。
持久性由插件负责后端会缓冲同步的 `session/event` 通知。检查点会在适配器分发前排空,在工具分发前刷写已记录的顶层工具调用,在 `agent/post-step` 刷写完整的响应与结果批次,并刷写最终的轮次结束。`SessionPersistence` 存储 `SessionEvent` `SessionHeader` 元数据JSONL 默认采用带校验和的 ZstandardSQLite 则遵循同一契约([决策](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md))。
`ctx.sessions.appendOutOfBand()` 会把纯日志事件加入开放轮次,或创建一个已刷写的零步骤轮次。`session/title` 按后写覆盖方式折叠,并携带源 seq来源信息;回退标题及其可选提供方都不会延迟响应。fork 会继承标题([决策](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md))。
`ctx.sessions.appendOutOfBand()` 会把插件所属的纯日志事件加入开放轮次,或创建一个平衡且已刷写的零步骤轮次。`session/title` 按后写覆盖方式折叠,并携带源 seq来源信息;其即时回退标题和唯一可选异步提供方都不会延迟 agent 响应。fork 会继承标题([决策](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md))。
### 模型内容

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
extension-cookbook.md: 056be4298ed2bec2b78ed777d58f1f8a60a34b78
extension-cookbook.zh.md: 41cdd4a7d14f32494d1dd5ae4a63c098d5640bdc
extension-cookbook.md: c13b46e06a3b34512cd371e6a4868a6e932a575f
extension-cookbook.zh.md: ef099ff318488ba26a3ba4fe736c5f320bdce4f3

View File

@@ -36,7 +36,7 @@ This waterfall is the reorderable policy layer. Use `ctx.tools.guard()` when an
## A UI plugin
A UI plugin renders from the `session/event` feed (the assistant token stream as `assistant/chunk`, plus turn/step boundaries and tool activity), and drives input back in via `agent.send()` / `agent.steer()`.
A UI plugin renders from the `session/event` feed (the assistant token stream as `assistant/chunk`, plus turn/step boundaries and tool activity), and drives input back in via `agent.followup()` / `agent.steer()`.
```ts
import type { Context } from 'cordis'
@@ -54,13 +54,13 @@ export function apply(ctx: Context) {
render(event.data.chunk.text)
}
})
onUserInput(text => ctx.agents.get(SessionId('client-session'))?.send([{ type: 'text', text }]))
onUserInput(text => ctx.agents.get(SessionId('client-session'))?.followup([{ type: 'text', text }]))
}
```
## A client-driver plugin (external protocol bridge)
A *client driver* is a UI plugin for a wire-protocol peer. It owns stdio, so stdout logging must be disabled, creates or resumes agents through the factory, maps harness events to protocol messages, and maps requests to `send()` or `cancel()`. Settle each request exactly once from durable `turn/end`, even if rendering fails, and tear agents down with `AgentHandle.dispose()` so disposal reaches quiescence.
A *client driver* is a UI plugin for a wire-protocol peer. It owns stdio, so stdout logging must be disabled, creates or resumes agents through the factory, maps harness events to protocol messages, and maps requests to `followup()` or `cancel()`. Settle each request exactly once from durable `turn/end`, even if rendering fails, and tear agents down with `AgentHandle.dispose()` so disposal reaches quiescence.
`packages/ui/acp` is the worked example: it bridges the agent to the Agent Client Protocol (JSON-RPC over stdio) so Zed and other ACP editors can drive it. See its README for the full method surface and the permission-prompt answerer it registers on the approval seam.
@@ -99,9 +99,9 @@ Every product feature maps to a listener on a documented extension seam — the
|---|---|
| Hook system (user + project level) | listeners on `agent/session-start`, `agent/prompt-submit`, `agent/request`, `agent/step-result`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation` — each interception waterfall returns a typed Decision; the `dsh-hooks-claude` / `dsh-hooks-codex` bridges map hook config files onto these seams |
| `/goal` | `ctx.goals` owns durable state, `dsh-goal-session` schedules same-session rounds through the public `Agent`, and separate command/tool producers expose human/model control |
| `/loop` | on the `turn/end` session event, `send()` the next iteration; or force-continue |
| `/loop` | on the `turn/end` session event, `followup()` the next iteration; or force-continue |
| Dynamic workflow | `ctx.workflows` + the worker-thread engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and terminal `agent/turn-stop` |
| Queued + steering messages | core `Agent.send()` / `Agent.steer()` |
| Queued + steering messages | core `Agent.followup()` / `Agent.steer()` |
| Context compaction (auto + manual) | the `ctx.compact` seam + `dsh-compact-basic`; automatic pressure runs on serial `agent/post-step`, canonical overflow recovery runs on `agent/request-error`, and manual callers use the same compact service ([compaction Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) — the model-facing `/compact` consumer tool is deferred) |
| System prompt configurability | `ctx.systemPrompt.section()` with ordering and scope-local shadowing |
| AGENTS.md (root) | a section provider reading the file |
@@ -118,8 +118,8 @@ Every product feature maps to a listener on a documented extension seam — the
| MCP | one plugin per server: discover tools → `ctx.tools.register()` |
| Skills | section + tool registration; `inject()` skill content on invocation |
| Memory | section provider + tool |
| Scheduled tasks (cron) | a plugin registers model-callable scheduling tools; timer fires → `send(…, {source: {kind: 'cron', …}})` when idle / `inject()` notification when busy |
| UI (GUI; CLI emits JSONL) | listen `session/event` (assistant chunks, boundaries, tool activity); input → `send()` |
| Scheduled tasks (cron) | a plugin registers model-callable scheduling tools; timer fires → `followup(…, {source: {kind: 'cron', …}})` when idle / `inject()` notification when busy |
| UI (GUI; CLI emits JSONL) | listen `session/event` (assistant chunks, boundaries, tool activity); input → `followup()` |
| Telemetry / replayable trace | `session/event` → JSONL; replay = `sessions.create(id, { seed })` |
| Model adapters | `LlmAdapter` subclass via `registerAdapter` (`dsh-llm-deepseek`, `dsh-llm-pi-ai`) |
| Plugin hot-reload | every registration is a `ctx.effect` → vendored HMR just works |

View File

@@ -36,7 +36,7 @@ export function apply(ctx: Context) {
## UI 插件
UI 插件从 `session/event` 事件流渲染(助手 token 流以 `assistant/chunk` 形式到达,加上轮次/步骤边界与工具活动),并通过 `agent.send()` / `agent.steer()` 将输入驱动回去。
UI 插件从 `session/event` 事件流渲染(助手 token 流以 `assistant/chunk` 形式到达,加上轮次/步骤边界与工具活动),并通过 `agent.followup()` / `agent.steer()` 将输入驱动回去。
```ts
import type { Context } from 'cordis'
@@ -54,13 +54,13 @@ export function apply(ctx: Context) {
render(event.data.chunk.text)
}
})
onUserInput(text => ctx.agents.get(SessionId('client-session'))?.send([{ type: 'text', text }]))
onUserInput(text => ctx.agents.get(SessionId('client-session'))?.followup([{ type: 'text', text }]))
}
```
## 客户端驱动插件(外部协议桥接)
*客户端驱动*是面向协议格式wire format对端的 UI 插件。它拥有 stdio因此必须禁用 stdout 日志;通过工厂创建或恢复 agent智能体将 harness 事件映射为协议消息;将请求映射为 `send()``cancel()`。每个请求从持久的 `turn/end` 恰好结算一次(即使渲染失败),并通过 `AgentHandle.dispose()` 拆除 agent 以使 dispose资源释放达到静止状态
*客户端驱动*是面向协议格式wire format对端的 UI 插件。它拥有 stdio因此必须禁用 stdout 日志;通过工厂创建或恢复 agent智能体将 harness 事件映射为协议消息;将请求映射为 `followup()``cancel()`。每个请求从持久的 `turn/end` 恰好结算一次(即使渲染失败),并通过 `AgentHandle.dispose()` 拆除 agent,确保 dispose资源释放流程完全停稳
`packages/ui/acp` 是完整的工作示例:它将 agent 桥接到 ACPAgent Client Protocol基于 stdio 的 JSON-RPC使 Zed 及其他 ACP 编辑器能够驱动它。其 README 描述了完整的方法接口以及它在审批 seam 上注册的权限提示应答器。
@@ -99,9 +99,9 @@ export function apply(ctx: Context) {
|---|---|
| 钩子系统(用户级 + 项目级) | `agent/session-start``agent/prompt-submit``agent/request``agent/step-result``tools/pre-execute``tools/post-execute``agent/turn-continuation` 上的监听器——每个拦截 waterfall 返回一个类型化 Decision`dsh-hooks-claude` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些 seam 上 |
| `/goal` | `ctx.goals` 管理持久状态,`dsh-goal-session` 通过公共 `Agent` 调度同会话回合,独立的命令/工具生产方分别提供人类/模型控制 |
| `/loop` | 在 `turn/end` 会话事件上 `send()` 下一次迭代;或强制继续 |
| `/loop` | 在 `turn/end` 会话事件上 `followup()` 下一次迭代;或强制继续 |
| 动态工作流 | `ctx.workflows` + worker-thread 引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的 prompt/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和终端 `agent/turn-stop` 来强制输出 |
| 排队消息 + steering中途引导 | 核心 `Agent.send()` / `Agent.steer()` |
| 排队消息 + steering中途引导 | 核心 `Agent.followup()` / `Agent.steer()` |
| 上下文压缩context compaction自动 + 手动) | `ctx.compact` seam + `dsh-compact-basic`;自动压力检查运行在串行 `agent/post-step`,规范化溢出恢复运行在 `agent/request-error`,手动调用方使用同一个压缩服务([压缩 Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)——面向模型的 `/compact` 消费方工具已推迟) |
| 系统提示词可配置性 | `ctx.systemPrompt.section()`,支持排序与作用域局部覆盖 |
| AGENTS.md根目录 | 一个读取该文件的 section provider |
@@ -118,8 +118,8 @@ export function apply(ctx: Context) {
| MCP | 每个服务器一个插件:发现工具 → `ctx.tools.register()` |
| Skill技能 | section + 工具注册;调用时通过 `inject()` 注入 skill 内容 |
| 记忆 | section provider + 工具 |
| 定时任务cron | 插件注册面向模型的调度工具;定时器触发 → 空闲时 `send(…, {source: {kind: 'cron', …}})`/忙碌时 `inject()` 通知 |
| UIGUICLI 输出 JSONL | 监听 `session/event`(助手分片、边界、工具活动);输入 → `send()` |
| 定时任务cron | 插件注册面向模型的调度工具;定时器触发 → 空闲时 `followup(…, {source: {kind: 'cron', …}})`/忙碌时 `inject()` 通知 |
| UIGUICLI 输出 JSONL | 监听 `session/event`(助手分片、边界、工具活动);输入 → `followup()` |
| 遥测 / 可回放 trace | `session/event` → JSONL回放 = `sessions.create(id, { seed })` |
| 模型适配器 | 通过 `registerAdapter` 注册 `LlmAdapter` 子类(`dsh-llm-deepseek``dsh-llm-pi-ai` |
| 插件热重载 | 每个注册都是一个 `ctx.effect` → vendor 的 HMR热模块替换直接生效 |

View File

@@ -32,7 +32,7 @@ Effective broad cancellation was requested, before queued/steering work is clear
Types: [Agent](../core-data-structures/core.md) · [AgentCancelCause](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:217`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:350`](../../packages/core/agent/src/types.ts)
### `agent/created` — emit
@@ -54,7 +54,7 @@ A fully configured agent and live session were published. Setup is composition-o
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:179`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:285`](../../packages/core/agent/src/types.ts)
### `agent/disposed` — emit
@@ -74,7 +74,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence but bef
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:188`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:294`](../../packages/core/agent/src/types.ts)
### `agent/error` — emit
@@ -96,7 +96,77 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:365`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:498`](../../packages/core/agent/src/types.ts)
### `agent/inbox/dequeue` — emit
The driver claimed one item out of the inbox: a queued item at a turn boundary, or steering drained between steps. Fires after the item leaves its FIFO and before it becomes a durable message.
```ts cordis-catalog
/**
* The driver claimed one item out of the inbox: a queued item at a turn
* boundary, or steering drained between steps. Fires after the item leaves
* its FIFO and before it becomes a durable message.
* @param agent - the agent whose inbox item was claimed.
* @param message - the claimed message (matching the `id` from its `agent/inbox/enqueue`).
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/dequeue'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void
```
Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:326`](../../packages/core/agent/src/types.ts)
### `agent/inbox/discard` — emit
Pending inbox items were dropped without delivering them, so every enqueued id receives exactly one terminal `agent/inbox/dequeue` OR `agent/inbox/discard`. Emitters: `cancel()` without `keepInbox` (after `agent/cancel-requested`, before the abort); a terminal `agent/turn-stop` dropping pending steering (in-turn and on the post-turn late-steering drain); and disposal of any still-pending items (before `agent/status('disposed')`). Fires once per drop with every dropped item.
```ts cordis-catalog
/**
* Pending inbox items were dropped without delivering them, so every
* enqueued id receives exactly one terminal `agent/inbox/dequeue` OR
* `agent/inbox/discard`. Emitters: `cancel()` without `keepInbox` (after
* `agent/cancel-requested`, before the abort); a terminal `agent/turn-stop`
* dropping pending steering (in-turn and on the post-turn late-steering
* drain); and disposal of any still-pending items (before
* `agent/status('disposed')`). Fires once per drop with every dropped item.
* @param agent - the agent whose inbox items were dropped.
* @param messages - the discarded messages in FIFO order (queued then steering); never empty.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/discard'(this: Scoped<Agent>, agent: Agent, messages: AgentMessage[]): void
```
Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:340`](../../packages/core/agent/src/types.ts)
### `agent/inbox/enqueue` — emit
A detached, frozen item entered the agent's inbox (queued or steering FIFO). Source defaults are already applied, so `message` holds the exact accepted values. This is the enqueue-time live signal; the durable record is the eventual `user/message`/`steering/message`. Injection through `agent.inject()` or equivalent `send()` routing bypasses the FIFOs and does not emit this.
```ts cordis-catalog
/**
* A detached, frozen item entered the agent's inbox (queued or steering
* FIFO). Source defaults are already applied, so `message` holds the exact
* accepted values. This is the enqueue-time live signal; the durable record
* is the eventual `user/message`/`steering/message`. Injection through
* `agent.inject()` or equivalent `send()` routing bypasses the FIFOs
* and does not emit this.
* @param agent - the agent whose inbox received the item.
* @param message - the accepted message (its returned `id`, content, source, contexts, steering, and wakeup facts).
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void
```
Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:316`](../../packages/core/agent/src/types.ts)
### `agent/post-step` — serial
@@ -119,7 +189,7 @@ Awaited serial checkpoint after the response, real or synthetic tool results, in
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:315`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:448`](../../packages/core/agent/src/types.ts)
### `agent/pre-step` — serial
@@ -142,7 +212,7 @@ Awaited serial checkpoint before `step/start`; appends land outside the pending
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:246`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:379`](../../packages/core/agent/src/types.ts)
### `agent/prompt-submit` — waterfall
@@ -169,28 +239,7 @@ Allow, rewrite, or block one claimed prompt before it becomes a user message. Ca
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:262`](../../packages/core/agent/src/types.ts)
### `agent/queued` — emit
Detached, frozen content entered the agent's inbox. Source defaults have already been applied, so these are the exact values retained for the log.
```ts cordis-catalog
/**
* Detached, frozen content entered the agent's inbox. Source defaults have
* already been applied, so these are the exact values retained for the log.
* @param agent - the agent whose inbox received the message.
* @param content - the accepted content blocks retained by the inbox.
* @param info - the accepted source, contexts, and whether it entered as steering.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/queued'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], info: { source: MessageSource; contexts: HookContext[]; steering: boolean }): void
```
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [HookContext](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:207`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:395`](../../packages/core/agent/src/types.ts)
### `agent/request` — waterfall
@@ -215,7 +264,7 @@ Replace the frozen call configuration. Model-visible content must use logged cha
Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:276`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:409`](../../packages/core/agent/src/types.ts)
### `agent/request-error` — waterfall
@@ -241,7 +290,7 @@ Recover a model-request failure after its failed step has closed. `retry` opens
Types: [Agent](../core-data-structures/core.md) · [LlmFailure](../core-data-structures/llm-streaming.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:330`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:463`](../../packages/core/agent/src/types.ts)
### `agent/session-prefix` — waterfall
@@ -267,7 +316,7 @@ Compose request-only messages placed before derived history. The frozen result i
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:291`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:424`](../../packages/core/agent/src/types.ts)
### `agent/session-start` — emit
@@ -289,16 +338,16 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:230`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:363`](../../packages/core/agent/src/types.ts)
### `agent/status` — emit
Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does not enter `running` synchronously; drive lifecycle from this event.
Agent status changed (`idle` ⇄ `running`, or → `disposed`). A waking delivery does not enter `running` synchronously; drive lifecycle from this event.
```ts cordis-catalog
/**
* Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does
* not enter `running` synchronously; drive lifecycle from this event.
* Agent status changed (`idle` ⇄ `running`, or → `disposed`). A waking
* delivery does not enter `running` synchronously; drive lifecycle from this event.
* @param agent - the agent whose status flipped.
* @param status - the status just entered (the transition's destination).
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
@@ -309,7 +358,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does no
Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:197`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:303`](../../packages/core/agent/src/types.ts)
### `agent/step-result` — waterfall
@@ -332,7 +381,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:303`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:436`](../../packages/core/agent/src/types.ts)
### `agent/turn-continuation` — waterfall
@@ -354,7 +403,7 @@ Override whether the turn continues. The default continues after tool calls or s
Types: [Agent](../core-data-structures/core.md) · [ContinuationDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:341`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:474`](../../packages/core/agent/src/types.ts)
### `agent/turn-stop` — serial
@@ -376,7 +425,7 @@ Monotonic terminal-stop checkpoint after continuation and steering are folded; a
Types: [Agent](../core-data-structures/core.md) · [ContinuationStop](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:352`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:485`](../../packages/core/agent/src/types.ts)
## `agent-loop/*`

View File

@@ -1253,7 +1253,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId):
Types: [CreateSessionOptions](../core-data-structures/persistence.md) · [OutOfBandSessionEventType](../core-data-structures/session.md) · [Session](../core-data-structures/session.md) · [SessionEvent](../core-data-structures/core.md) · [SessionEventMap](../core-data-structures/session.md) · [SessionId](../core-data-structures/core.md) · [TurnTrigger](../core-data-structures/session.md)
Source: [`packages/core/session/src/index.ts:605`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:606`](../../packages/core/session/src/index.ts)
## `ctx.sessionTitle` — `SessionTitleService`

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
cordis-primer.md: ee65e6e702ecaeb506ce7334032c38e09c936cda
cordis-primer.zh.md: ee4f6864ba7864fc95b5eb8e31acbcaea6e99825

View File

@@ -1,5 +1,7 @@
# Cordis Primer
English | [中文](cordis-primer.zh.md)
Cordis is the vendored plugin framework underneath the DeepSeek Harness SDK. This primer teaches the Cordis ideas a harness plugin author needs before reading the generated [events](cordis-catalog/events.md) and [services](cordis-catalog/services.md) catalogs; the [Cordis tutorial](cordis-tutorial/index.md) walks the same ideas hands-on. The vendored source and sync procedure live in [vendor/README.md](../vendor/README.md).
## Cordis In Five Ideas

48
docs/cordis-primer.zh.md Normal file
View File

@@ -0,0 +1,48 @@
# Cordis 入门
[English](cordis-primer.md) | 中文
Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。本文介绍 harness 插件作者在阅读生成的[事件](cordis-catalog/events.md)与[服务](cordis-catalog/services.md)目录之前需要了解的 Cordis 核心概念;[Cordis 教程](cordis-tutorial/index.md)则通过实践逐一讲解这些概念。vendor 源码与同步流程见 [vendor/README.md](../vendor/README.md)。
## 五个核心概念
- **插件是实现 Service 的对象。** 它可以是一个带有可选 `inject``apply(ctx)` 字段的函数,也可以是一个 `Service` 子类,其生命周期由 Cordis 挂载到当前上下文中。
- **上下文是服务的容器。** 一个服务占据一个稳定的 `ctx.<key>`(如 `ctx.tools``ctx.llm``ctx.sessions`);其他插件通过 key 查找服务,而非导入具体实现。
- **通过 `inject` 声明服务依赖。** 插件声明所需的服务后,会等待这些服务就绪才启动;加载顺序通过服务依赖表达,而非手动编排启动序列。
- **类型化事件用于通信。** 服务通过 TypeScript 声明合并注册事件名,然后以 `emit``waterfall`(瀑布式事件)、`parallel``serial` 方式分发,分别对应监听者观察、包装、并行扇出或按序执行。
- **注册是可逆的副作用。** 提示词片段、工具 schema、适配器、提供方和监听器通过 `ctx.effect()``ctx.on()` 安装reload 和 teardown 时可预期地回卷。
## 分发模式
每个事件具有以下分发模式之一,且只能通过对应方法分发。
| 模式 | 是否 await | 分发顺序 | 是否有返回值? |
|---|---|---|---|
| `emit` | 否 | 监听器按注册顺序观察 | 否 |
| `waterfall` | 否 | 监听器按注册顺序观察 | 是 |
| `parallel` | 是 | 所有监听器并行观察事件 | 否 |
| `serial` | 是 | 监听器按注册顺序观察 | 是 |
分发模式是事件公开契约的一部分。新的 harness 事件通过 `@mode` 标签记录模式,以便生成的目录可以将声明与分发调用点做交叉校验。
<a id="cordis-waterfall-semantics"></a>
## Cordis Waterfall 语义
`ctx.waterfall` 是环绕中间件。监听器接收 `(...args, next)`。调用 `next()` 会执行下游监听器;下游返回值通过 `next()` 返回当前包装层,可由该层包装后继续向外返回。不调用 `next()` 直接返回则短路。
协作式监听器通常修改一个共享的请求或决策对象,然后委托。监听器也可以选择完全替换结果,下游监听器将只看到替换后的结果。仅当监听器必须在普通注册之前运行时才使用 `prepend: true`
对于单决策事件,短路是设计意图。策略监听器在拥有决策权时可以不调用 `next()` 直接返回,而仅做标注或观察的监听器则必须委托。
<a id="loader-configuration"></a>
## Loader 配置
`@cordisjs/plugin-include``!!js` 解析为表达式节点,但 Loader 仅在挂载插件前对条目的 `config` 做插值。条目元数据(`id``name``group``disabled``inject``intercept``isolate`)保持字面值;因此 `disabled: !!js ...` 是一个 truthy 对象,会始终禁用该条目。需要根据环境选择挂载哪些插件时,请使用显式的配置覆盖层。
## 实践规则
将行为封装为插件:工具流水线事件属于 `ctx.tools`,模型流式输出属于 `ctx.llm`,实时 agent智能体协调属于 `ctx.agents`。拦截和策略优先使用事件;直接能力调用优先使用服务方法。
每个注册都应有对应的 disposerdispose资源释放函数要么从 `ctx.effect()` 返回一个,要么使用 Cordis 提供的辅助方法自动处理。如果 teardown 顺序有要求,请将相关工作放在同一个 effect 中,以确保资源释放按预期顺序回卷。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
approval.md: c9b411e38508fc7e8ac2e2dd923d9c8cc2f475a3
approval.zh.md: c3d67b195a0c04186911285ccb76cd89eceb421b

View File

@@ -1,5 +1,7 @@
# User Approval
English | [中文](approval.zh.md)
The user-approval seam of [dsh-user-approval](../../packages/ui/user-approval) answers one question: may this specific action proceed? It owns the shared request/outcome vocabulary, the `ctx.approval` dispatch service, the `approval/request` answerer waterfall, the log-only audit pair, and the per-session `ask`/`never` policy. UI channels such as [dsh-acp](../../packages/ui/acp) provide answerers; callers such as [dsh-tools](../../packages/core/tools) and [dsh-tool-bash](../../packages/bash/tool-bash) consume the closed outcome and fail closed unless it is `allowed-once`.
Source: [`packages/ui/user-approval/src/index.ts`](../../packages/ui/user-approval/src/index.ts)

View File

@@ -0,0 +1,90 @@
# 用户审批
[English](approval.md) | 中文
[dsh-user-approval](../../packages/ui/user-approval) 的用户审批 seam 回答一个问题:这个具体操作是否可以继续?它拥有共享的请求/结果词汇、`ctx.approval` 分发服务、`approval/request` 应答者 waterfall瀑布式事件、仅记录日志的审计事件对以及按会话的 `ask`/`never` 策略。UI 通道如 [dsh-acp](../../packages/ui/acp) 提供应答者;调用方如 [dsh-tools](../../packages/core/tools) 和 [dsh-tool-bash](../../packages/bash/tool-bash) 消费闭合的结果,除非结果为 `allowed-once`,否则一律拒绝。
源码:[`packages/ui/user-approval/src/index.ts`](../../packages/ui/user-approval/src/index.ts)
## 标识与结果
每个请求都会获得一个全新的 `ApprovalRequestId`。该品牌类型将 `approval/asked``approval/decided` 审计事件配对,同时不会让审批 id 与工具调用 id 或 agent智能体/会话 id 互换。
```ts type-equiv
/**
* Pairs one `approval/asked` audit event with its `approval/decided`.
* Service-issued (one fresh id per {@link ApprovalService.request} call).
*/
type ApprovalRequestId = Branded<'ApprovalRequestId'>
```
`ApprovalOutcome` 是闭合的,且默认拒绝。`allowed-once` 仅授权所询问的那一个操作;调用方对 `rejected`、`cancelled` 和 `unavailable` 均执行拒绝。缺失、无所有权、抛异常或不合规的应答者会产生 `unavailable`,而非放行。
```ts type-equiv
/**
* Closed approval outcomes: a one-shot grant, explicit rejection, withdrawn
* request, or unavailable answerer. Callers fail closed on `unavailable`.
*/
type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
```
## 按会话策略
`ApprovalPolicy` 决定在交互式应答者运行之前发生什么。`ask` 委托给组合的应答者链,链的无应答默认值为 `unavailable``never` 确定性地返回 `rejected`,不分发任何应答者。生效值为会话日志中最后一条 `approval/policy` 事件,回退到服务配置。`setApprovalPolicy(session, policy)` 是唯一的写入路径,因此回放能重建覆盖值。
```ts type-equiv
/**
* A session's approval policy — what happens to an {@link ApprovalService}
* ask BEFORE any interactive answerer sees it:
*
* - `'ask'` (the default) — delegate to the composed answerers; with none
* composed the chain falls through to the fail-closed `'unavailable'`
* (exactly today's behavior).
* - `'never'` — never prompt anyone: every ask resolves `'rejected'`
* deterministically. The strict headless stance (CI, unattended runs) and
* the only policy value stated in the system prompt — unlike `'ask'`, its
* outcome is knowable without asking, so stating it cannot overclaim.
*/
type ApprovalPolicy = 'ask' | 'never'
```
提示词段落会声明 `never` 的确定性行为,并以服务自有的标记记录当前策略。重启后,步骤前叙述器从已记录的请求头中读取该标记,而非从部署 persona 行文中推断状态。ACPAgent Client Protocol空闲切换会在 bridge 中保持,直到下一个 `turn/start`,因为审批审计和策略事件必须保持在轮次内,以确保持久回放的正确性。
## 审批请求
`ApprovalRequest` 以足够精确的方式标识 agent 和工具操作,以便路由和审计该问题。它有意省略工具参数:应答者通过 `callId` 将提示附加到已流式输出的工具调用上,而非渲染一份可能漂移的副本。
```ts type-equiv
/**
* Readonly same-process permission question. `callId` links to an already
* presented tool call, so arguments are not duplicated here.
*/
interface ApprovalRequest {
/**
* The agent on whose behalf the question is asked. Routes the question (a
* UI answerer only answers for agents it owns) and receives the audit
* events on its session log.
*/
readonly agent: Agent
/** The tool the question is about (presentation and audit). */
readonly toolName: string
/**
* The exact tool call being decided, when the asker has one — lets a UI
* attach the prompt to the tool call it already streamed.
*/
readonly callId?: CallId
/** The asker's human-readable explanation of WHY it is asking. */
readonly reason?: string
/**
* Aborting withdraws the question: the request settles `'cancelled'`
* immediately and a late answer from a still-pending answerer is discarded.
*/
readonly signal?: AbortSignal
}
```
## 分发与审计
`ctx.approval.request(req)` 要求发起请求的会话处于一个打开的轮次内。它追加 `approval/asked`,获取一个结果,追加对应的 `approval/decided`,然后以该结果 resolve。`never` 策略在服务内部、waterfall 分发之前强制执行,因此即使后来以 `prepend` 注册的应答者也无法绕过它。应答者在拥有该请求时返回结果,否则调用 `next()` 委托;第一个应答占据唯一的决策槽位。
审计事件仅写入日志,不进入模型 transcript文本记录。模型可见的行为是调用方派生的工具结果而请求头记录的是模型实际看到的提示词策略。服务 dispose资源释放时会一并移除其提示词段落和步骤前叙述器应答者监听器独立地通过 effect 绑定到其所属插件。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
bash.md: 35cf2061588907dde41123efb01e453eb9cc929d
bash.zh.md: 0cfeb9e1a858f7057e720215c41a757588751122

View File

@@ -1,5 +1,7 @@
# Bash Executor
English | [中文](bash.zh.md)
The bash execution seam is split across interface ([dsh-bash](../../packages/bash/bash), `ctx.bash`), implementations ([dsh-bash-local](../../packages/bash/bash-local) and [dsh-bash-sandbox](../../packages/bash/bash-sandbox)), and consumer ([dsh-tool-bash](../../packages/bash/tool-bash), the `bash` schema). Generic background-task ids, ownership, and controls live in [tasks.md](tasks.md); this seam returns a task-free process handle.
Source: [`packages/bash/bash/src/types.ts`](../../packages/bash/bash/src/types.ts)

View File

@@ -0,0 +1,241 @@
# Bash 执行器
[English](bash.md) | 中文
bash 执行 seam 分为接口([dsh-bash](../../packages/bash/bash)`ctx.bash`)、实现([dsh-bash-local](../../packages/bash/bash-local) 与 [dsh-bash-sandbox](../../packages/bash/bash-sandbox))和消费方([dsh-tool-bash](../../packages/bash/tool-bash),即 `bash` schema。通用后台任务的 id、所有权与控制位于 [tasks.md](tasks.md);本 seam 返回一个不含任务概念的进程句柄。
源码:[`packages/bash/bash/src/types.ts`](../../packages/bash/bash/src/types.ts)
## 受管 shell 环境命名空间
`DSH_*` 变量是归 Harness 所有的子进程事实。面向模型的 bash 工具通过 `ctx.bashEnv` 收集它们,再经由 `BashExecRequest.dshEnv` 传递;执行器在合并当前快照之前会移除继承而来的 `DSH_*` 名称。
```ts type-equiv
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}`
```
```ts type-equiv
/** Trusted DeepSeek Harness variables for one bash execution. */
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>
```
## 请求与规格:`resolve()` 拆分
该 seam 将**面向模型/插件的请求**`workdir`/`timeoutMs`/`stdoutMaxBytes` 可选,由配置或请求策略补全)与执行器实际使用的**完全解析后的 spec**(这些字段均为必填)分开。工具层在二者之间调用 `ctx.bash.resolve(request)`——这具体落实了仓库的「包package seam 上显式优于隐式」规则:`BashExecSpec` 的读者不必猜测工作目录或输出预算来自何处。
```ts type-equiv
/**
* A caller's execution REQUEST: `workdir` and `timeoutMs` are optional and
* filled by {@link BashExecutor.resolve} from the implementation's config.
* This is the model-/plugin-facing shape; pass it to `resolve()` to obtain a
* fully-resolved {@link BashExecSpec}.
*/
interface BashExecRequest {
command: string
/** Working directory override (default: implementation-configured). */
workdir?: string | undefined
/** Timeout override in milliseconds (implementations cap it). */
timeoutMs?: number | undefined
/**
* Foreground stdout capture budget in bytes. Absent uses the executor's
* default output cap. Trusted in-process consumers use this when they must
* parse complete stdout up to their own bounded limit; the model-facing bash
* tool does not expose it as a parameter.
*/
stdoutMaxBytes?: number | undefined
/** Abort signal — implementations kill the command when it fires. */
signal?: AbortSignal | undefined
/**
* Bytes to write to the command's stdin, then close it. Absent leaves stdin
* closed/empty (the default for model-driven tool calls). Set by in-process
* plugins (e.g. the hooks bridges, which write a hook command's JSON payload
* to its stdin); the model-facing bash tool does not expose it as a parameter
* (a model that needs stdin uses shell syntax like a heredoc or a pipe).
*/
stdin?: string | undefined
/**
* Ordinary environment entries for the command, merged after the credential
* scrub. `DSH_*` is reserved for {@link dshEnv} and implementations reject it
* here. Set by in-process plugins (the hooks bridges set
* `CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, …); the model-facing bash tool
* does not expose it as a parameter.
*/
env?: Record<string, string> | undefined
/**
* Harness-owned `DSH_*` variables for this execution. Executors discard
* ambient `DSH_*` entries before merging this snapshot, so an unavailable
* current fact cannot inherit a stale value from the harness process, and
* reject non-`DSH_*` names supplied through this managed channel.
*/
dshEnv?: DshEnvironment | undefined
/** Fully resolved per-call sandbox policy; sandboxing executors default it. */
sandboxPolicy?: SandboxExecutionPolicy | undefined
}
```
```ts type-equiv
/**
* A resolved execution spec. {@link BashExecutor.resolve} fills and caps the
* required fields; {@link BashExecutor.start} ignores `timeoutMs` because
* background processes have no executor timeout.
*/
interface BashExecSpec {
command: string
workdir: string
timeoutMs: number
/**
* Resolved foreground stdout capture budget in bytes. `run()` uses it for
* stdout; background tasks and stderr keep the executor's own output cap.
*/
stdoutMaxBytes: number
/** Abort signal — implementations kill the command when it fires. */
signal?: AbortSignal | undefined
/** Bytes to write to stdin before closing it; absent means no stdin. */
stdin?: string | undefined
/**
* Ordinary environment entries carried through from
* {@link BashExecRequest.env}. `DSH_*` remains reserved for {@link dshEnv}.
* OPTIONAL on the spec for the same reason as `stdin`: absent means no
* ordinary extra environment.
*/
env?: Record<string, string> | undefined
/** Managed `DSH_*` snapshot; implementations reject ordinary names. */
dshEnv?: DshEnvironment | undefined
/** Resolved sandbox policy; ignored by executors that do not confine. */
sandboxPolicy: SandboxExecutionPolicy | undefined
}
```
`stdin` 和 `env` 是受信任的进程内插件输入,不由 `dsh-tool-bash` 暴露。本地执行器会先清除环境中的凭据,再合并调用方显式提供的 env。见 [bash-stdin-env Agent Noteagent 决策记录)](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
`stdoutMaxBytes` 同样仅供受信任插件使用。它让前台消费方能在有界解析预算内请求完整 stdout而不会改变 stderr、后台任务或面向模型的 bash 工具的常规输出上限。
## 前台运行:`BashRunResult`
一次已完成(或被终止)的前台运行的结果。正交的结果**独立报告**:一个进程可以同时超时并以退出码 0 退出(因为它捕获了信号),因此 `timedOut`、`aborted`、`signal` 和 `exitCode` 各自独立为一个字段;调用方永远不会把一次被截断的运行误读为干净的成功。
```ts type-equiv
/** The outcome of one completed (or killed) foreground run. */
interface BashRunResult {
/** Exit code; null when the process died from a signal. */
exitCode: number | null
/** Terminating signal (e.g. 'SIGTERM'); null on normal exit. */
signal: NodeJS.Signals | null
/**
* True when the executor's own timeout was the FIRST cause to cut the command
* short. Mutually exclusive with {@link aborted}: one fused deadline drives
* both the timeout and the caller's cancellation, so a timeout and an abort
* racing before process close report the single first-abort cause, not both
* (see the [timeout-library Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)).
*/
timedOut: boolean
/**
* True when the caller's `AbortSignal` was the FIRST cause to kill the command
* (and it was not the executor's own timeout). Mutually exclusive with
* {@link timedOut} — see there for the first-cause classification.
*/
aborted: boolean
/** The effective timeout applied to this run (after defaulting/capping). */
timeoutMs: number
stdout: CollectedOutput
stderr: CollectedOutput
/** Sandbox execution facts, absent for an unsandboxed executor. */
sandbox?: BashSandboxInfo
}
```
每个流是一个 `CollectedOutput`:(可能被截断的)文本加恢复信息。截断时,`text` 是**尾部**,完整流溢出到一个私有文件:
```ts type-equiv
/** One captured stream: the (possibly truncated) text plus recovery info. */
interface CollectedOutput {
/** Collected text — the TAIL of the stream when truncated. */
text: string
/** True when bytes were dropped from `text`. */
truncated: boolean
/** Path to a file holding the COMPLETE stream, when truncated and available. */
spillPath?: string
}
```
## 文件沙箱:`BashSandboxInfo`
使用沙箱的执行器通过 `BashExecutor.sandboxMode` 暴露其已配置的模式回退值。工具层请求 [`@deepseek-ai/dsh-sandbox-policy`](../../packages/sandbox/sandbox-policy/README.md),把每个调用会话的持久 `sandbox/mode` 覆盖值与不可变 cwd 解析为 `BashExecRequest.sandboxPolicy`;经用户批准、严格更宽松的调用只替换模式。模式/root/enforcement 词汇归 [`@deepseek-ai/dsh-sandbox` 沙箱 seam](sandbox.md) 所有;模式仅管辖文件效果。
沙箱化运行会报告其模式、保守的拒绝分类与强制执行完整度。`runnerFailed` 标记命令运行前沙箱 runner 已失败;前台执行会抛出 `SANDBOX_UNAVAILABLE`,而已结束的后台进程只能通过其事实通道报告。
```ts type-equiv
/**
* Sandbox facts for one run, present iff a sandboxing executor handled it.
* Facts are reported independently of process exit status so callers can
* distinguish command failures from policy denials and runner failures.
*/
interface BashSandboxInfo {
/** The mode the command actually ran under. */
mode: SandboxMode
/** Whether the sandbox denied a file operation. */
denied: boolean
/** How completely the selected runner enforced the requested mode. */
enforcement?: SandboxEnforcement
/** Whether the sandbox runner failed before the command could run. */
runnerFailed?: boolean
}
```
最后一项补全了这套词汇:当受限模式没有可用后端时,`ctx.sandbox` 提供方会抛出、执行器会传播由[沙箱 seam](sandbox.md)所有的 `SANDBOX_UNAVAILABLE` 错误码。选定的 runner 拒绝其 profile 时会触达同一个故障关闭的前台错误;已结束的后台任务则记录 `runnerFailed`。模型会在结果中收到拒绝/runner 事实,仅当拒绝标记指出生效模式时才得知该模式,并可通过 `sandbox_permissions` 加 `justification` 请求一次性、严格更宽松的重试;执行任何操作前,`ctx.approval` 必须批准该次确切调用。完整的策略与切换设计见[沙箱 Agent Note](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
## 后台进程:`BashProcess`
`start()` 返回不含 id 或所有者的句柄。`dsh-tool-bash` 将它适配为 `ctx.tasks.start()` 钩子;随后由通用运行时拥有任务标识与生命周期。`done` 在进程关闭时 resolve 且绝不 reject进程结束后仍可读取并且沙箱事实会在 `done` resolve 前写入。
```ts type-equiv
/**
* A background process handle returned by {@link BashExecutor.start}. It is the
* only access path; buffered output remains readable after exit. Executor
* disposal kills running processes and awaits {@link done}.
*/
interface BashProcess {
/** Process lifecycle state (settled exactly once). */
status: BashProcessStatus
/** Exit code once finished (null = killed by signal / still running). */
exitCode: number | null
/** Terminating signal name, when signal-killed. */
signal: NodeJS.Signals | null
/** Resolves when the underlying process closes (never rejects — a spawn failure settles as `killed` with the error on stderr). */
readonly done: Promise<void>
/** Sandbox facts, stamped once a confined process settles. */
sandbox?: BashSandboxInfo
/**
* Read output produced since the previous read (consuming — consecutive
* reads never re-deliver). Reads that lost data flag `lossy` and point at
* full-stream spill files when available.
*/
readOutput(): BashProcessRead
/**
* Kill the process group. Returns false when it had already finished
* (no-op); idempotent.
*/
kill(): boolean
}
```
`readOutput()` 返回增量 delta 与 spill 恢复事实:
```ts type-equiv
/** One incremental {@link BashProcess.readOutput} read. */
interface BashProcessRead {
/** Output produced since the previous read (stderr in a marked section). */
delta: string
/** True when truncation dropped unread bytes the delta cannot include. */
lossy: boolean
/** Full stdout spill file, when stdout truncation occurred and a safe path is available. */
stdoutSpillPath?: string
/** Full stderr spill file, when stderr truncation occurred and a safe path is available. */
stderrSpillPath?: string
}
```
## 服务
`BashExecutor` 拥有 `resolve`、前台 `run`、后台进程 `start` 以及 `sandboxMode` 能力事实。`dsh-bash-local` 拥有进程组、超时/中止处理、有界收集器、spill 文件、凭据清除以及 dispose资源释放后完全停稳。`dsh-tool-bash` 拥有面向模型的渲染,并将后台句柄适配到[通用任务运行时](tasks.md)。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
code-runtime.md: 64de3c45d4f1d1d981daa6c6f074abb667e0aa52
code-runtime.zh.md: 4b14aeb2183010e8140540258ce8109df9f59910

View File

@@ -1,5 +1,7 @@
# Code Runtime
English | [中文](code-runtime.zh.md)
The code-execution seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) whose interface ([dsh-code-runtime](../../packages/code-runtime/code-runtime), `ctx.codeRuntime`) runs one model-written program against host-provided async bindings and reports what it printed and returned. Code execution is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). Backends differ by execution substrate and source language, both readonly descriptors on the service; the worker-thread backend and tool-registry consumer are specified by the [Code Mode foundation](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) and [typed-return contract](../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md).
Source: [`packages/code-runtime/code-runtime/src/types.ts`](../../packages/code-runtime/code-runtime/src/types.ts)

View File

@@ -0,0 +1,147 @@
# 代码运行时
[English](code-runtime.md) | 中文
代码执行 seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):其接口([dsh-code-runtime](../../packages/code-runtime/code-runtime)`ctx.codeRuntime`)针对宿主提供的异步 binding 运行一段模型编写的程序,并报告其打印内容与返回值。代码执行是**一项可选能力**,不属于 agent loop智能体循环主干因此其词汇定义在此而非 [core.md](core.md) 中。各后端的执行基底与源语言不同这两项均为服务上的只读描述符worker-thread 后端与工具注册表消费方的契约见 [Code Mode 基础设计](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)和[类型化返回契约](../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md)。
源码:[`packages/code-runtime/code-runtime/src/types.ts`](../../packages/code-runtime/code-runtime/src/types.ts)
## 运行:请求进,结果出
`CodeRunRequest` 携带**运行时所需的一切**。按照「包package边界处显式优于隐式」的规则默认值时间预算、输出上限来自实现的已校验配置绝不是 `run()` 内部隐藏的 `??`
```ts type-equiv
/**
* One run: the program source plus everything the runtime acts on. Per the
* explicit-over-implicit convention, defaulting (time budgets, output caps)
* is the implementation's validated config — a request carries no optional
* tuning knobs for a hidden `??` to fill in.
*/
interface CodeRunRequest {
/**
* The program source, in the runtime's {@link ../index.ts | language}. It
* runs as the body of an async function: top-level `await` and `return`
* are available, and the completion value becomes
* {@link CodeRunResult.value}.
*/
program: string
/** Host functions exposed to the program, one global object per namespace. */
bindings: CodeBindingNamespace[]
/**
* Abort the run: the runtime stops the program (hard, even mid-loop) and
* resolves with a {@link CodeRunFailure} of kind `'abort'`. In-flight
* binding calls are the CALLER's to settle — the runtime only stops asking.
*/
signal?: AbortSignal
}
```
结果将错误报告为一个**字段**,而非 `run()` 的 rejection。报告失败的程序是调用方的职责不走异常路径与 `BashExecutor.run` 的 resolve-on-failure 契约一致):
```ts type-equiv
/**
* The outcome of one run. An error is a FIELD on a resolved result, never a
* rejection of `run()` — reporting a failed program is the caller's job, not
* an exception path.
*/
interface CodeRunResult {
/**
* The program's completion value (its top-level `return`), when it ran to
* completion and the value crossed the runtime's lossless-JSON boundary.
* Invalid or over-limit completions fail the run instead of substituting a
* rendered string; a failed or value-less run leaves this absent.
*/
value?: CodeJsonValue
/** Text the program emitted, in order, bounded only as part of the outer result. */
logs: string[]
/** Present iff the run failed; see {@link CodeRunFailure} for the taxonomy. */
error?: CodeRunFailure
}
```
## 绑定:宿主函数作为程序全局变量
每个 `CodeBindingNamespace` 在程序内成为一个由异步可调用函数组成的全局对象Code Mode 消费方传入一个:`tools`)。参数与返回值必须是无损 JSON且跨越边界时不受 seam 层字节上限约束;运行时可以通过结构化克隆桥接它们。命名空间可以声明程序可见的错误类,而无需让运行时知道消费方的名称:运行时会注入真实构造函数,并将被拒绝的调用转为该类的实例。运行时也将绑定名视为不可信输入(`__proto__` 是普通自有属性,绝不会发生原型碰撞):
```ts type-equiv
/**
* Program-visible typed rejection for one binding namespace. The runtime
* injects a real error constructor under `name`; rejected member calls become
* its instances and expose the exact member name through
* `memberNameProperty`. Both strings are runtime data rather than knowledge
* of a particular consumer such as Code Mode.
*/
interface CodeBindingErrorClass {
/** Constructor global and resulting `Error.name` (must be a usable JS identifier). */
name: string
/** Non-empty own property for the member name; cannot replace `name`, `message`, or `stack`. */
memberNameProperty: string
}
```
```ts type-equiv
/**
* A named group of {@link CodeBindingFunction}s the runtime exposes to the
* program as one global object (e.g. `tools`). Function names are arbitrary
* strings — a runtime must treat names like `__proto__` or `constructor` as
* ordinary own properties (null-prototype construction), never as prototype
* collisions.
*/
interface CodeBindingNamespace {
/** The global identifier the program sees (must be a valid JS identifier). */
global: string
/** The callable members, keyed by the exact name the program calls. */
functions: Record<string, CodeBindingFunction>
/** Optional program-visible typed rejection contract for this namespace. */
errorClass?: CodeBindingErrorClass
}
```
```ts type-equiv
/** A lossless JSON value transferable across the dependency-light code-runtime seam. */
type CodeJsonValue = null | boolean | number | string | CodeJsonValue[] | { [key: string]: CodeJsonValue }
```
```ts type-equiv
/**
* One host-side function exposed to the program as an async callable. The
* runtime bridges calls to it (possibly across a serialization boundary), so
* `args` and the resolution value MUST be lossless JSON. A runtime rejects a
* lossy or non-cloneable value with a descriptive error rather than corrupting
* the run. No seam-level byte cap applies to a binding resolution. A rejection
* of this function surfaces inside the program as a rejection of the
* corresponding call.
*/
type CodeBindingFunction = (args: unknown) => Promise<CodeJsonValue>
```
## 捕获的输出与失败分类体系
日志是按发出顺序排列的纯字符串。运行时捕获程序的 console 与流输出,但通道和 console 方法的元数据不属于 seam因为消费方只渲染文本。实现会对序列化后的外层日志数组以及完成值或失败消息的组合载荷设置上限固定的结果封装语法与消费方展示空白不计入这份可变载荷计量。超限会显式失败而不会在值中插入替代内容。
失败类型是**正交的结果,独立报告**(见 [defensive-patterns](../defensive-patterns.md)):预算耗尽不是异常,中止不是超时,基底崩溃(如 OOM也不是二者中的任何一个
```ts type-equiv
/**
* Why a run failed. The kinds are orthogonal outcomes reported independently
* (per docs/defensive-patterns.md): a budget expiry is not an exception, an
* abort is not a timeout, and a substrate death is neither.
*
* - `'exception'` — the program threw or failed to parse/transform.
* - `'timeout'` — an implementation-owned budget expired; the message says which.
* - `'abort'` — {@link CodeRunRequest.signal} fired.
* - `'worker-exit'` — the execution substrate died without settling (e.g. OOM).
* - `'invalid-output'` — the completion value was not lossless JSON.
* - `'output-limit'` — the serialized outer logs/value/diagnostic exceeded the configured cap.
*/
interface CodeRunFailure {
/** The failure class (see the interface doc for each kind's meaning). */
kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'
/** Human-readable detail, suitable for feeding back to a model to self-correct. */
message: string
}
```
## 服务
`CodeRuntime``ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))由 `run(request)` 加两个只读描述符组成:`language`(程序必须使用的语言,`'typescript'` 是已知值;生成语言相关展示的消费方据此切换,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底,`'worker-thread'`、`'process'`、`'container'`;仅为诊断标签,**不构成安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),并在 dispose资源释放时等待系统完全停稳teardown 完成前,进行中的运行都已终止并等待结束。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
compaction.md: 71bbe7d9c17c6a3d35684a6c87d3072ab4f88df1
compaction.zh.md: 35e9c9ef0050f01c5249d1502bb2782511acc819

View File

@@ -1,5 +1,7 @@
# Compaction
English | [中文](compaction.zh.md)
The compaction seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) split like bash: interface ([dsh-compact](../../packages/compact/compact), `ctx.compact`), implementation (a backend such as [dsh-compact-basic](../../packages/compact/compact-basic)), and consumer (a `/compact` tool, deferred). Compaction is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A tokenizer- or template-based backend is a sibling package implementing the same interface. Unlike bash, the interface necessarily depends on `dsh-session` and `dsh-llm`: its verbs act on an agent-owned `Session`, and its durable summary event uses the `ContentBlock` vocabulary (see the [compaction capability-seam Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)).
Source: [`packages/compact/compact/src/types.ts`](../../packages/compact/compact/src/types.ts)

View File

@@ -0,0 +1,97 @@
# 压缩compaction
[English](compaction.md) | 中文
压缩 seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md),与 bash 一样分为接口([dsh-compact](../../packages/compact/compact)`ctx.compact`)、实现(例如 [dsh-compact-basic](../../packages/compact/compact-basic) 后端)和消费方(延期实现的 `/compact` 工具)。压缩是**一项可选能力**,不属于 agent loop智能体循环主干因此其词汇定义在此而非 [core.md](core.md) 中。基于 tokenizer 或模板的后端是实现同一接口的兄弟包package。与 bash 不同,该接口必然依赖 `dsh-session``dsh-llm`:其动词作用于 agent 所有的 `Session`,而其持久摘要事件使用 `ContentBlock` 词汇(见[压缩能力 seam Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md))。
源码:[`packages/compact/compact/src/types.ts`](../../packages/compact/compact/src/types.ts)
## `compact/*` 会话事件
压缩通过声明合并为 [`SessionEventMap`](session.md) 扩展三种事件类型。三者都**仅写入日志**——记录压缩锁及其 provenance绝不进入 surface。这里有意不扩展 `SurfaceEventType`(只有产生消息的事件才到达模型),因此摘要本身承载在另一条带有 `surfaceOp: { op: 'replace', start, end }``user/message` 上——这是摘要压缩执行的唯一 surface 变更。关于复用 `user/message` 为何是如实建模而非权宜之计,见对应 Agent Note。
| 事件 | 载荷 | 作用 |
|---|---|---|
| `compact/start` | `{ turn }` | 获取日志记录的锁 |
| `compact/summary` | `{ summary, shadowedRange, shadowedSeqs, shadowedTokenCount, provider, model, maxTokens? }` | provenance摘要块、被遮蔽的 surface 边界对(`start`/`end` seq——位置跨度而非数值区间、按 surface 顺序排列的被遮蔽 seq、估算 token 数,以及摘要调用的 envelope`provider``model`,若有生成上限则还包括该上限)——写入日志后,该一次性请求可由日志 + 代码重建(见可重建性 Agent Note |
| `compact/end` | `{ turn, error? }` | 释放锁(摘要调用抛出异常时设置 `error` |
锁括住**整个**操作:先追加 `compact/start`,然后执行摘要生成、写入 `compact/summary` 来源记录与 `user/message` 替换,最后才追加 `compact/end`。最后释放锁意味着操作中途崩溃会表现为可检测的遗留锁(有 `compact/start` 而无匹配的 `compact/end`),而非一个虚假声称压缩已完成的 `compact/end`
这些变体在 `declare module '@deepseek-ai/dsh-session'` 块内合并,因此——与其他子页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。
## `CompactionResult`
成功压缩向调用方返回:记账事件 seq、原始摘要、被遮蔽的范围与 seq以及估算 token 数。
```ts type-equiv
/** Result of a successful compaction operation. */
interface CompactionResult {
/** The seq of the appended `compact/start` event. */
startSeq: number
/** The seq of the appended `compact/summary` event. */
summarySeq: number
/** The seq of the appended `compact/end` event. */
endSeq: number
/** The summary content blocks produced by the backend. */
summary: ContentBlock[]
/**
* The surface-boundary pair that was shadowed: the seqs of the first
* (`start`) and last (`end`) surface nodes of the replaced range. A
* surface-POSITION span, not a numeric seq interval — after a prior replace
* lands a fresh high-seq summary node at an older range's position, `start`
* can be GREATER than `end`. {@link CompactionResult.shadowedSeqs} is the
* authoritative set of shadowed nodes, in surface order.
*/
shadowedRange: { start: number; end: number }
/** The seqs of all shadowed surface nodes, in surface order. */
shadowedSeqs: number[]
/** Estimated token count of the shadowed content. */
shadowedTokenCount: number
}
```
## 服务
自动调用方会说明策略为何运行;实现可以比普通压力更激进地处理已确认的溢出。
```ts type-equiv
/** Why automatic policy is asking a backend to consider compaction. */
type CompactionTrigger = 'pressure' | 'context-overflow'
```
`CompactService` 暴露 `compactIfNeeded(agent, trigger, signal)` 以执行自动 `pressure` 或 `context-overflow` 策略;没有可安全执行的工作时返回 `null`。它还针对显式、两端均包含的 surface 范围暴露 `compactRegion(...)`。每个后端都使用包导出的 `COMPACT_CHECKPOINT_SOURCE` 标记其替换用的 `user/message`;消费方调用 `isCompactCheckpointSource()`,而不是把检查点识别逻辑耦合到某一个后端。实现必须把传入的 signal 转发给摘要流程。该 seam 不拥有计价 API单例 [`ctx.tokenMeter`](token-meter.md) 直接拥有估算与回放,而 `dsh-compact-basic` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。
压力压缩在串行 `agent/post-step` 中运行:此时成功的 assistant 输出、工具结果、缓冲上下文和 steering中途引导已持久化但 `step/end` 尚未发生。一旦压力或规范化溢出满足条件compact-basic 会在选择范围前调用可选的 [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md),再通过 `ctx.tokenMeter` 重新测量,并且可以在不生成摘要的情况下推进 surface。失败请求的恢复在失败的步骤关闭后通过 `agent/request-error` 运行;仅当 surface replacement generation 前进时才批准一个带新编号的步骤重试,即便后续摘要工作在剪枝后抛异常亦如此;取消仍然优先。区域边界保持工具调用/结果配对,但不保持整个轮次,因此一个过大轮次中较早关闭的步骤可以被压缩。`dsh-compact-basic` 拥有阈值、保留尾部策略、溢出上限与失败处理。
该 seam 导出 `toolPairingBalancedBefore(session, seq)` 与 `toolPairingBalancedAfter(session, seq)`,用于这些边缘检查。两者都会验证当前 surface 成员关系,并拒绝缺失的 seq 与遗留结果;其缓存语义由[包契约](../../packages/compact/compact/README.md#tool-pairing-boundaries)规定。
## 工具结果剪枝产出
可选的工具结果剪枝服务会报告每次持久内容替换以及 Unicode code point 的总减少量。其公开结果类型位于 [`compact-tool-result-prune/src/types.ts`](../../packages/compact/compact-tool-result-prune/src/types.ts)。
```ts type-equiv
/** Provenance and size accounting for one landed surface replacement. */
interface PrunedEntry {
/** Full-fidelity tool-result event shadowed by the replacement. */
readonly originalSeq: number
/** Newly appended pruned tool-result event. */
readonly replacementSeq: number
/** Tool call shared by the original and replacement. */
readonly callId: CallId
/** Original text size in Unicode code points. */
readonly charsBefore: number
/** Replacement text size in Unicode code points. */
readonly charsAfter: number
}
```
```ts type-equiv
/** Aggregate outcome of one stable-surface pruning pass. */
interface PruneResult {
/** Replacements in the snapshotted surface order. */
readonly pruned: readonly PrunedEntry[]
/** Total Unicode code points removed across replacements. */
readonly charsRemoved: number
}
```

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
core.md: 781267cccdb5bbda33e5be6a9e807fdbe47dbc83
core.zh.md: d0f67983b98b0cf679a8e599a5f8ab3c64490dd0

View File

@@ -1,5 +1,7 @@
# Core Data Structures
English | [中文](core.zh.md)
This folder catalogs the **data structures** of the DeepSeek Harness — what each core type represents, its literal shape, and where the full detail lives. It complements [architecture.md](../architecture.md), which describes *behavior* (the service map, the session/turn/step lifecycle, the event taxonomy); this page describes the *vocabulary* that behavior moves around.
## What counts as "core"
@@ -163,6 +165,8 @@ Adapters emit a raw **chunk** protocol; the loop logs the chunks (replay fidelit
The full union, the adapter contract (usage-before-finish, raw-JSON tool arguments, the two sanctioned error paths), and `BlockAssembler` live on **[llm-streaming.md](llm-streaming.md)**.
<a id="the-model-request-and-result"></a>
## The model request
One model call is a fully-assembled `GenerateOptions`. The adapter answers with a raw `StreamChunk` stream; the consumer assembles it with `BlockAssembler` (see [llm-streaming.md](llm-streaming.md)).
@@ -323,7 +327,7 @@ Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/t
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`, `context/message`, `steering/message`).
* `assistant/message`, `tool/result`, `steering/message`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
@@ -351,7 +355,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
}[T]
```
The fourteen event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/message`, `prompt/blocked`, `context/message`, `assistant/chunk`, `assistant/message`, `tool/call`, `tool/result`, `steering/message`, `todo/write`, `request/header`), the `deriveMessages()` projection rules, the `TurnTrigger`/`TurnEndReason` reasons, and the turn-enclosure invariant are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` seam, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**.
The thirteen event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/message`, `prompt/blocked`, `assistant/chunk`, `assistant/message`, `tool/call`, `tool/result`, `steering/message`, `todo/write`, `request/header`), the `deriveMessages()` projection rules, the `TurnTrigger`/`TurnEndReason` reasons, and the turn-enclosure invariant are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` seam, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**.
## The agent handle
@@ -361,8 +365,9 @@ Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types
```ts type-equiv
/**
* Message options. An omitted source attests direct human input as `{ kind: 'user' }`
* and may authorize policy consumers, so non-human producers must label their content.
* Options for {@link Agent.followup}, {@link Agent.queue}, and {@link Agent.steer}.
* An omitted source attests direct human input as `{ kind: 'user' }` and may
* authorize policy consumers, so non-human producers must label their content.
*/
interface SendOptions {
source?: MessageSource
@@ -372,19 +377,90 @@ interface SendOptions {
* records them directly at its next checkpoint.
*/
contexts?: HookContext[]
/** Opaque JSON state retained on the durable message but hidden from the model. */
meta?: JsonValue
}
```
`InjectOptions` accepts ordinary message attribution and durable model-hidden JSON metadata. Attached contexts belong only to queued or steering input, so synthetic injection cannot accept them:
```ts type-equiv
/** Options specific to durable synthetic context injection. */
interface InjectOptions extends Omit<SendOptions, 'contexts'> {
/** Opaque JSON state retained in the session event but hidden from the model. */
interface InjectOptions {
/** Defaults to `{ kind: 'plugin', plugin: '' }`; non-human producers should identify themselves. */
source?: MessageSource
/** Opaque JSON state retained on the durable message but hidden from the model. */
meta?: JsonValue
}
```
The advanced acceptance form makes every default explicit and rules out attached contexts on injection:
```ts type-equiv
/**
* Fully specified input for {@link Agent.send}. Unlike the intent-named
* helpers, this form applies no defaults: callers provide content, source,
* contexts, metadata (including explicit `undefined`), target, and wakeup.
* The union excludes attached contexts from non-waking next-step injection.
*/
type ResolvedAgentInput = {
content: ContentBlock[]
source: MessageSource
meta: JsonValue | undefined
} & (
| { target: 'next-turn'; wakeup: boolean; contexts: HookContext[] }
| { target: 'next-step'; wakeup: true; contexts: HookContext[] }
| { target: 'next-step'; wakeup: false; contexts: [] }
)
```
FIFO delivery methods return an opaque `AgentMessageId`, stable across that message's `agent/inbox/*` events. Injection returns an id but bypasses those events:
```ts type-equiv
/**
* Opaque id assigned to one accepted agent input. FIFO inputs carry the same id
* on their `agent/inbox/*` events; injection bypasses those events.
*/
type AgentMessageId = Branded<'AgentMessageId'>
```
The `agent/inbox/*` live events carry one accepted message; injection bypasses the FIFOs and never appears on them:
```ts type-equiv
/**
* One accepted FIFO message, carried by the `agent/inbox/*` live events. `id`
* is the value returned by the accepting helper or {@link Agent.send},
* stable across this message's enqueue, dequeue, and discard events. Source
* defaults, when applicable, are already applied, so these are the exact values
* the item was accepted with.
* `steering` is true for an item drained between steps; otherwise it is claimed
* at a turn boundary. `SendOptions.meta` is intentionally omitted: it is durable
* model-hidden state that lands on the eventual `user/message`/
* `steering/message`, not live-event routing data.
*/
interface AgentMessage {
/** The id returned by the accepting helper or {@link Agent.send}. */
id: AgentMessageId
content: ContentBlock[]
source: MessageSource
contexts: HookContext[]
/** Whether the item joined the steering FIFO rather than the queued FIFO. */
steering: boolean
/** Whether the item wakes the driver or requests another step. */
wakeup: boolean
}
```
```ts type-equiv
/** Options for {@link Agent.cancel}. */
interface CancelOptions {
/**
* Preserve queued and steering inbox items instead of discarding them. The
* active turn is still aborted, but un-started and pending work survives for a
* later turn and no `agent/inbox/discard` fires.
*/
keepInbox?: boolean
}
```
```ts type-equiv
/** Stable runtime cause accepted by {@link Agent.cancel}. */
type AgentCancelCause =
@@ -392,65 +468,103 @@ type AgentCancelCause =
| { readonly kind: 'parent' }
```
The structural `Agent` interface exposes four intent helpers plus the fully resolved acceptance method. The concrete driver implements the matrix once, and each helper supplies its fixed routing and defaults.
```ts type-equiv
/** Public agent handle; its concrete implementation is internal to `@deepseek-ai/dsh-agent-loop`. */
interface Agent {
/** The single identity shared with {@link session}. */
readonly id: SessionId
/** The provider route and model this agent's requests use. */
readonly options: AgentOptions
/** The live session this agent drives; its log is the durable source of truth. */
readonly session: Session
/** The current lifecycle state, mirrored on every `agent/status` transition. */
readonly status: AgentStatus
/** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
readonly ctx: Context
/**
* Queue one detached, frozen lossless-JSON item. If claimed, it is the sole
* ordinary message in its FIFO-ordered turn; the next claimed item waits for
* that turn's checkpoint.
* Attached contexts share the same snapshot and ownership boundary. Invalid
* input throws synchronously before notification or enqueue.
* Queue an ordinary message as its own FIFO-ordered turn and wake the driver.
* Content, resolved source, and attached contexts are detached, validated,
* and frozen together; invalid input throws synchronously before notification
* or enqueue.
* @param content - the prompt content blocks.
* @param options - source, attached contexts, and durable model-hidden meta.
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
*/
send(content: ContentBlock[], options?: SendOptions): void
followup(content: ContentBlock[], options?: SendOptions): AgentMessageId
/**
* Submit steering while the agent is `running`. An open turn records it at
* the next steering checkpoint before a request or continuation decision;
* policy may stop before another step. After turn close and its checkpoint,
* any remainder is queued for a later turn; terminal `agent/turn-stop`,
* cancellation, or disposal may discard it. Uses the same synchronous
* snapshot-and-validation boundary as {@link send}; when idle, delegates to it.
* Queue an ordinary message without waking an idle driver. The item retains
* FIFO order and is claimed only after another input wakes the driver. A lone
* queued item leaves `whenIdle()` resolved.
* @param content - the prompt content blocks.
* @param options - source, attached contexts, and durable model-hidden meta.
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
*/
steer(content: ContentBlock[], options?: SendOptions): void
queue(content: ContentBlock[], options?: SendOptions): AgentMessageId
/**
* Submit steering into the running turn and request another step. An open turn
* records it at the next steering checkpoint before a request or continuation
* decision; policy may stop before another step. After turn close and its
* checkpoint, any remainder is queued for a later turn; terminal
* `agent/turn-stop`, cancellation, or disposal may discard it. Idle steering
* becomes a waking ordinary turn.
* @param content - the steering content blocks.
* @param options - source, attached contexts, and durable model-hidden meta.
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
*/
steer(content: ContentBlock[], options?: SendOptions): AgentMessageId
/**
* Append detached model-facing context without running the model. An open-turn
* injection joins at the current log position unless the current tool batch is
* executing; then it waits FIFO until that batch settles and drains before turn
* close even when interrupted. Idle injection uses a one-shot turn and durability
* checkpoint. Disposal awaits idle checkpoints; flush failures report through `agent/error`.
* executing; then it waits FIFO until that batch settles and drains before
* turn close even when interrupted. Idle injection uses a one-shot turn and
* durability checkpoint. Disposal awaits idle checkpoints; flush failures
* report through `agent/error`. An omitted source defaults to
* `{ kind: 'plugin', plugin: '' }`.
* @param content - the injected context content blocks.
* @param options - source and durable model-hidden meta.
* @returns the accepted injection's {@link AgentMessageId}; injection emits no `agent/inbox/*` events.
*/
inject(content: ContentBlock[], options?: InjectOptions): void
inject(content: ContentBlock[], options?: InjectOptions): AgentMessageId
/**
* Clear all queued and steering work, including items waiting to start, and
* abort the active turn. An effective call first emits
* `agent/cancel-requested` with the resolved typed cause. The first cause wins
* for the active turn, and `whenIdle()` resolves after cancellation reaches
* quiescence. Omission means `{ kind: 'user' }`. Idle cancellation is a no-op
* and does not arm later work. The active turn snapshots and freezes the cause.
* @param cause - the stable caller intent carried by the current turn signal.
* Accept one fully specified input through the same snapshot and routing path
* as the four intent-named helpers. `next-turn` targets the ordinary FIFO;
* `next-step`/wakeup targets steering (falling back to an ordinary waking turn
* while idle); and `next-step` without wakeup injects durable context without
* running the model. Every field is mandatory and no source or routing default
* is applied. Invalid input throws synchronously before notification, enqueue,
* or append.
* @param input - the resolved content, attribution, context, metadata, and routing facts.
* @returns the accepted input's {@link AgentMessageId}, carried by FIFO lifecycle events when applicable.
*/
cancel(cause?: AgentCancelCause): void
send(input: ResolvedAgentInput): AgentMessageId
/**
* Clear queued and steering work — unless `keepInbox` — and abort the active
* turn. An effective call first emits `agent/cancel-requested` with the
* resolved typed cause. The first cause wins for the active turn, and
* `whenIdle()` resolves after cancellation reaches quiescence. Omitted cause
* means `{ kind: 'user' }`. Idle cancellation is a no-op and does not arm
* later work. The active turn snapshots and freezes the cause.
* @param cause - the stable caller intent carried by the current turn signal.
* @param options - cancellation options; `keepInbox` preserves pending work.
*/
cancel(cause?: AgentCancelCause, options?: CancelOptions): void
/** Resolve at idle quiescence; disposal waits for driver exit rather than only the status transition. */
whenIdle(): Promise<void>
}
```
`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `running` describes the driver-wide drain interval, which can span turn close, its durability checkpoint, and consecutive queued turns; it does not prove a turn is still open. `AgentOptions` is merge-extensible: core declares `provider?` and `model?` (dispatch requires both after `agent/request`). Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
The cause is a TypeScript-enforced same-process input. An active holder copies its discriminant into the runtime-only `AbortSignal.reason`; it is retired before `turn/end` publication. `agentInterruptReasonOf(signal)` recognizes `user`, `parent`, and lifecycle-only `disposed` without consulting ambient initiator state. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result.
The cause is a TypeScript-enforced same-process input. An active `TurnCancellation` holder copies its discriminant into the runtime-only `AbortSignal.reason` and is retired before `turn/end` publication; the frozen `AbortSignal.reason` remains readable after that retirement. `agentInterruptReasonOf(signal)` recognizes `user`, `parent`, and lifecycle-only `disposed` without consulting ambient initiator state. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result.
The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits.
@@ -460,7 +574,7 @@ The process-local initiator carried by `ctx.agents` is the exact `Agent` above,
## Interception decisions
Each `agent/*` interception waterfall returns a small, seam-specific typed union — the unified Decision idiom (the tool seams' `PreToolDecision`/`PostToolDecision` in [tools.md](tools.md) follow the same shape). A CC/Codex hook bridge maps its `permissionDecision`/`decision`/`continue`/`additionalContext` fields onto these; a native plugin returns them directly. Prompt and post-tool decisions share one model-facing context shape, `HookContext`, which carries a REQUIRED `source` (a missing source would default to `{kind:'user'}` and mislabel plugin context as a user prompt). Its `content` reaches the model verbatim as user-role input, while JSON `meta` persists plugin state without exposing it to the model. Absent or `separate` placement becomes `context/message`; `prompt-prefix` placement is available to prompt and steering inbox attachments and bakes the context before the effective request in the same message. Both decisions carry `additionalContexts[]` so every entry preserves its own provenance, metadata, and placement. Continuation reasons are steering messages instead and deliberately use the narrower content/source shape.
Each `agent/*` interception waterfall returns a small, seam-specific typed union — the unified Decision idiom (the tool seams' `PreToolDecision`/`PostToolDecision` in [tools.md](tools.md) follow the same shape). A CC/Codex hook bridge maps its `permissionDecision`/`decision`/`continue`/`additionalContext` fields onto these; a native plugin returns them directly. Prompt and post-tool decisions share one model-facing context shape, `HookContext`, which carries a REQUIRED `source` (a missing source would default to `{kind:'user'}` and mislabel plugin context as a user prompt). Its `content` reaches the model verbatim as user-role input, while JSON `meta` persists plugin state without exposing it to the model. Absent or `separate` placement becomes an injected `user/message` (plugin/goal source); `prompt-prefix` placement is available to prompt and steering inbox attachments and bakes the context before the effective request in the same message. Both decisions carry `additionalContexts[]` so every entry preserves its own provenance, metadata, and placement. Continuation reasons are steering messages instead and deliberately use the narrower content/source shape.
Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
@@ -470,8 +584,8 @@ interface HookContext {
content: ContentBlock[]
source: MessageSource
/**
* Model placement. Absent or `separate` records an independent
* `context/message`; `prompt-prefix` prepends this context and a stable
* Model placement. Absent or `separate` records an independent injected
* `user/message`; `prompt-prefix` prepends this context and a stable
* request delimiter to the same user-role message as its attached prompt.
*/
placement?: 'separate' | 'prompt-prefix'

View File

@@ -0,0 +1,671 @@
# 核心数据结构
[English](core.md) | 中文
本目录编目 DeepSeek Harness 的**数据结构**:每个核心类型代表什么、它的字面形状,以及完整细节在哪里。它与 [architecture.md](../architecture.md) 互补——后者描述*行为*(服务映射、会话/轮次/步骤生命周期、事件分类体系);本页描述行为所操作的*词汇*。
## 什么算"核心"
harness 是一个微内核:一个极小的核心加上众多插件。大多数类型属于某一个插件或某一项能力。但有少数类型构成**主干**——agent loop智能体循环及其事件在*每一个*轮次中使用的语言,无论加载了哪些可选插件。这些就是"核心"。
精确地说,一个数据结构是**核心**的,当且仅当满足以下条件之一:
1. 它流经 agent loop 主干——循环在每个轮次中持有、派生、流式输出或记录它(`Message``StreamChunk``SessionEvent``Agent` 句柄本身),与当前加载了哪些插件无关;**或者**
2. 它是插件作者面向某条流水线编写的代表性类型——`ToolDefinition`(每个工具*是什么*)。
其他一切都记录在**子页面**上,而非本页。划线的规则是:*你编写、持有或接收的类型是核心;为它提供类型推导、渲染或持久化的机制是子页面细节*。因此 `ToolDefinition` 是核心,但为它提供类型推导的 `ValueSchemaSpec`/`ParameterSchemaSpec` 机制、为它提供渲染意图的 `ToolCallView`/`ToolResultView` 词汇,以及存储事件日志的 `SessionPersistence` seam 都不是——它们分别在下列子页面中。
| 子页面 | 负责内容 |
|---|---|
| [llm-streaming.md](llm-streaming.md) | `StreamChunk` 协议格式wire format+ 适配器契约adapter contract`BlockAssembler``LlmAdapter` seam |
| [token-meter.md](token-meter.md) | 不可变的标量与位置回放度量,附带已消费日志修订号 |
| [scope.md](scope.md) | 作用域注册标识、dispatch 载体,以及拥有的 `Scope` 上下文 |
| [goal.md](goal.md) | 持久 goal 标识、生命周期快照、激活、变更记录与 Round 归属 |
| [commands.md](commands.md) | 人类命令 seam定义、适配器发现、直接调用、结果与解析视图 |
| [session.md](session.md) | 完整的 `SessionEventMap` 变体目录、`TurnTrigger`/`TurnEndReason``deriveMessages()`、轮次封闭不变式 |
| [persistence.md](persistence.md) | 持久性 seam`SessionPersistence`、JSONL + SQLite 后端、`session/flush`、崩溃恢复、`SessionHeader` |
| [session-query.md](session-query.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 |
| [session-title.md](session-title.md) | 持久标题快照、来源 provenance 与异步提供方契约 |
| [system-prompt.md](system-prompt.md) | 逐次组装的上下文、工具提供方结果、提示词段落与协作式组装 |
| [tools.md](tools.md) | `ToolDefinition` 完整字段、schema DSL、`ToolExecution`/`ToolResult`、工具展示 UI 类型,以及受保护的执行流水线 |
| [user-interaction.md](user-interaction.md) | UI 支持的人工问答 seam`AskUserQuestionRequest`、answer/options 词汇、提供方 API、错误分类体系 |
| [approval.md](approval.md) | 一次性用户审批 seam`ApprovalRequest``ApprovalOutcome`、逐会话策略、审计与 answerer 契约 |
| [bash.md](bash.md) | bash 执行器 seam`BashExecRequest`/`Spec``BashRunResult`、后台 `BashProcess` 句柄 |
| [pty.md](pty.md) | 持久化终端 ID、后端/会话契约、发送就绪状态、有界读取与 owner 可见快照 |
| [sandbox.md](sandbox.md) | 每会话策略解析与进程约束 seam文件效果模式、执行/提供方策略、`ConfinedArgv`、强制执行与故障关闭错误 |
| [code-runtime.md](code-runtime.md) | 代码执行 seam`CodeRunRequest`/`Result`、绑定命名空间、捕获日志、`CodeRunFailure` 分类体系 |
| [filesystem.md](filesystem.md) | 文件系统 seam`FsTarget`、读/写/编辑结果、观测到的文件状态、`FsErrorCode` |
| [lsp.md](lsp.md) | LSP 导航 seam`LspQueryRequest`/`Result``LspProvider`/`Service`、四种操作、`LspError` |
| [skills.md](skills.md) | skill技能服务发现优先级、`SkillSummary`/`SkillDefinition`、会话前缀目录、面向模型的 `skill` 加载 |
| [compaction.md](compaction.md) | 压缩compactionseam`compact/*` 会话事件、`CompactionResult``CompactService` 接口 |
| [subagent.md](subagent.md) | subagent seam命名提供方注册表、`SubagentStartRequest`/`Result`/`Run`、启动时与运行时能力拆分 |
| [web.md](web.md) | Web 访问 seam`WebSearchRequest`/`Result``WebFetchRequest`/`Result``WebFetchBody`、提供方可用性、`WebError` |
| [spill.md](spill.md) | spill 存储 seam`SaveTextSpill``SpillOwner`/`SpillSource``SpillRef`、品牌类型 `SpillLocator` |
| [workflow.md](workflow.md) | 工作流 seam`WorkflowStartRequest``WorkflowMeta``WorkflowRun`/`Result``workflow/*` 事件载荷、`WorkflowError` 致命性 |
> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务使用生成的[服务目录](../cordis-catalog/services.md)。
<a id="the-map--derived-union-pattern"></a>
## `…Map → derived-union` 模式
harness 中几乎所有可扩展的和类型都遵循同一形状:一个以判别标签为键的接口(`…Map`),联合类型由 `keyof` 派生。插件通过**声明合并**添加变体——无需修改拥有该类型的包package
```ts ignore-check
// The pattern, schematically:
interface ThingMap {
'a': { kind: 'a'; /* … */ }
'b': { kind: 'b'; /* … */ }
}
type ThingKind = keyof ThingMap // 'a' | 'b'
type Thing = ThingMap[keyof ThingMap] // the discriminated union
// A plugin extends it without touching the source package:
declare module '@deepseek-ai/dsh-llm' {
interface ThingMap {
'c': { kind: 'c'; /* … */ }
}
}
```
六个规范 map 使用此模式;插件作者扩展它们:
| Map | 包 | 派生 | 目录 |
|---|---|---|---|
| `ContentBlockMap` | dsh-llm | `ContentBlock` | [下文](#content-blocks-and-messages) |
| `MessageSourceMap` | dsh-llm | `MessageSource` | [下文](#content-blocks-and-messages) |
| `FinishReasonMap` | dsh-llm | `FinishReason` | [下文](#the-model-request-and-result) |
| `TurnTriggerMap` | dsh-session | `TurnTrigger` | [session.md](session.md) |
| `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.md) |
| `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.md) |
消费方最常 `switch` 的两个大型判别联合类型是:**`StreamChunk`**(流式协议)和 **`SessionEvent`**(日志条目)。按仓库约定,对标签做 `switch`——不要链式 `if`——这样每个分支都能窄化类型,拼错的标签会编译失败。
<a id="branded-ids"></a>
## 品牌化 ID
跨越包边界的 ID 都经过**品牌化**——结构上是字符串,但在类型层面不可互换(不能把 `SessionId` 传给需要 `CallId` 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。
`Branded<B>` 原语位于独立的纯类型包 [dsh-brand](../../packages/util/brand) 中(没有运行时代码,也不依赖 Harness 包),因此任何包都能品牌化其拥有的 id而无需依赖无关的能力包。
源码:[`packages/util/brand/src/index.ts`](../../packages/util/brand/src/index.ts)
```ts type-equiv
/** A string carrying a compile-time-only brand `B`. */
type Branded<B extends string> = string & { readonly [BRAND]: B }
```
两个核心 ID 是 `CallId`关联工具调用及其结果dsh-llm和 `SessionId`(活跃 agent 与持久会话共享的标识dsh-session。能力包也会品牌化各自的 id例如 [tasks.md](tasks.md) 中的 `TaskId`。
<a id="content-blocks-and-messages"></a>
## 内容块与消息
一段对话由 `Message` 组成;一条消息是一个类型化**内容块**的数组。块的联合类型从 `ContentBlockMap` 派生。
源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
```ts type-equiv
/**
* Merge-extensible content blocks keyed by `type`. New core blocks must land
* with adapter, UI, and compaction support.
*/
interface ContentBlockMap {
'text': TextBlock
'reasoning': ReasoningBlock
'tool-call': ToolCallBlock
'tool-result': ToolResultBlock
}
```
各块接口(完整字段见源码):`TextBlock``text`)、`ReasoningBlock`thinking区别于可见文本、`ToolCallBlock``id: CallId`、`name`、原始 JSON `arguments`)、`ToolResultBlock``toolCallId`、嵌套 `content: ContentBlock[]`、`isError?`)。`ContentBlock = ContentBlockMap[ContentBlockType]`。核心集仅限于每条交付路径都尊重的块——多模态内容(图像、音频等)没有核心块类型;需要的功能通过可合并扩展的 map 添加,同时提供适配器/UI/压缩支持。
`Message` 由角色和块组成。由循环派生的 assistant 消息携带其持久提供方/模型标识,以及可选的适配器私有回放元数据:
```ts type-equiv
/** Provider ownership and adapter-private replay data for an assistant message. */
interface AssistantProvenance {
/** Provider route that produced the message. */
provider: string
/** Provider model id that produced the message. */
model: string
/**
* Lossless-JSON adapter state needed to replay the provider response.
* `LlmService` exposes it to a target adapter only when that adapter instance
* currently owns both this historical provider and the target provider.
*/
replayState?: unknown
}
```
```ts type-equiv
/**
* A single message in a conversation history. Loop-derived assistant messages
* always carry provenance; callers may omit it on hand-built foreign history.
*/
interface Message {
role: 'system' | 'user' | 'assistant'
content: ContentBlock[]
/** Present only on assistant messages produced by a routed adapter. */
provenance?: AssistantProvenance
}
```
消息来源本身也是一个可合并扩展的和类型:
```ts type-equiv
/**
* Where a message (or injected content) came from.
* Merge-extensible sum type — plugins add their own `kind`s.
*/
interface MessageSourceMap {
user: { kind: 'user' }
plugin: { kind: 'plugin'; plugin: string }
}
```
## 流式输出
适配器发出原始**分片**协议;循环记录分片(回放保真度),同时将同一批分片送入 `BlockAssembler` 以重建块和消息。`StreamChunk` 是基于 `type` 的封闭判别联合——`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`。
完整联合类型、适配器契约usage-before-finish、原始 JSON 工具参数、两条认可的错误路径)和 `BlockAssembler` 在 **[llm-streaming.md](llm-streaming.md)** 中。
<a id="the-model-request-and-result"></a>
## 模型请求
一次模型调用是一个完全组装好的 `GenerateOptions`。适配器以原始 `StreamChunk` 流作答;消费方用 `BlockAssembler` 组装它(见 [llm-streaming.md](llm-streaming.md))。
源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
提供方与模型发现使用小型、提供方无关的描述符。模型目录仅供参考:路由仍以已注册提供方为键,适配器也可以接受未列出的模型 id。
```ts type-equiv
/** Display metadata for one registered provider route. */
interface LlmProviderInfo {
/** Provider route key used by {@link GenerateOptions.provider}. */
id: string
/** Human-readable provider name for selectors and diagnostics. */
name: string
}
```
```ts type-equiv
/** One adapter-discovered model; catalog membership is advisory, not request validation. */
interface LlmModelInfo {
/** Provider route that owns this model entry. */
provider: string
/** Model id passed to {@link GenerateOptions.model}. */
id: string
/** Human-readable model name for selectors. */
name: string
/** Optional user-facing distinction from otherwise similar models. */
description?: string
}
```
对正确性敏感的模型容量与参考目录分开查询,并归服务该确切路由的适配器所有。
```ts type-equiv
/** Provider-owned context capacity for one exact provider/model route. */
interface LlmModelContext {
/** Maximum combined request and response context in tokens. */
contextWindow: number
}
```
```ts type-equiv
/** A single model request, fully assembled. */
interface GenerateOptions {
/** Registered provider route selecting the adapter instance. */
provider: string
model: string
/**
* Ordered conversation messages, exactly as the provider sees them (after
* the `system` slot). A loop-built request assembles them as
* `EpochHeader.messagePrefix` + the derived history (dsh-agent-loop); a
* hand-built one-shot passes any list.
*/
messages: Message[]
/** System prompt text (adapters map to the provider's system slot). */
system?: string
/** Tool schemas (adapters map to the provider's `tools` field). */
tools?: ToolSchema[]
temperature?: number
maxTokens?: number
/**
* Stop sequences: generation halts as soon as the model produces any one of
* these strings (adapters map to the provider's stop field, e.g. OpenAI
* `stop`). The stop string itself is not included in the output.
*/
stop?: string[]
signal?: AbortSignal
/**
* Session identity stamped by the loop for listener routing. Adapters ignore
* it; replay uses it to keep concurrent parent and child cursors independent.
*/
sessionId?: Branded<'SessionId'>
/**
* Provider-neutral classification for an auxiliary model call. Adapters may
* map the purpose to model-hidden transport metadata or purpose-specific
* generation policy. Ordinary conversation requests leave it unset.
*/
purpose?: 'compaction' | 'session-title'
}
```
模型响应为何停止由可合并扩展的原因表示。提供方终态失败携带流式契约的 [`LlmFailure`](llm-streaming.md#llmfailure)
```ts type-equiv
/**
* Why a model response stopped.
* Merge-extensible so adapters can surface provider-specific reasons.
*/
interface FinishReasonMap {
'stop': { kind: 'stop' }
'tool-calls': { kind: 'tool-calls' }
'max-tokens': { kind: 'max-tokens' }
'aborted': { kind: 'aborted'; failure: LlmFailure }
'error': { kind: 'error'; failure: LlmFailure }
}
```
`FinishReason = FinishReasonMap[keyof FinishReasonMap]`。`TokenUsage`(逐调用计量,含不相交的缓存字段)详见 [llm-streaming.md](llm-streaming.md)。
`GenerateOptions.tools` 携带 `ToolSchema`——工具的 JSON Schema 描述,发送给模型。它声明在 dsh-llm而非 dsh-tools正是因为它是循环每一步组装请求的一部分
```ts type-equiv
/**
* JSON-schema description of a tool, as sent to the model.
*
* Declared here (not in dsh-tools) because it is part of {@link GenerateOptions};
* dsh-tools' ToolDefinition and dsh-system-prompt's PromptAssembly both import
* it from this package.
*/
interface ToolSchema {
name: string
description: string
/** JSON Schema object for the arguments. */
parameters: Record<string, unknown>
}
```
面向模型的 `ToolSchema` 是协议格式;产出它的已注册 `ToolDefinition`schema + `execute`)在 [tools.md](tools.md) 中。
### 请求信封:`LlmCallConfig` 与记录的 header
循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、渲染后的提示词、权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)以及会话前缀。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Noteagent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型或采样参数。`agent/session-prefix` 为每个循环实例组合一次仅用于请求的 prefix 消息header 记录实际使用的确切结果。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
在协议格式上,循环构建的请求按此顺序读取:`system` 槽位(渲染后的提示词组装)→ `messagePrefix`(冻结的会话前缀)→ 派生历史——边界快照,其尾部在轮次首步是最新的 `user/message`,在后续步骤是上一步的工具结果。前缀从不进入派生历史;它的持久记录是 header 事件,开发不变式针对每个循环构建的请求精确重算此等式。
FIXME(call-config-shape):重新审视此类型的精确定义——出于缓存目的,哪些字段确实属于 epoch 层级(`model` 肯定属于;采样标量目前出于谨慎放在这里),以及适配器需要时,提供方特有的额外项(推理选项、额外 body 参数)应归属何处。
```ts type-equiv
/**
* Provider + model + sampling scalars of one conversation's requests. Every field maps
* 1:1 onto the same-named `GenerateOptions` field; the loop builds requests
* from the logged header rather than accepting these per call.
*/
interface LlmCallConfig {
provider: string
model: string
temperature?: number
maxTokens?: number
stop?: string[]
}
```
## 会话
`Session` 是一份类型化 `SessionEvent` 的**仅追加日志**——唯一的真源。LLM大语言模型消息历史从日志*派生*`deriveMessages()`),而非单独存储。事件词汇从 `SessionEventMap` 派生:
源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
```ts type-equiv
/**
* One immutable entry in the session log.
*
* A proper discriminated union over `type` (not independent `type`/`data`
* unions), so `switch (event.type)` narrows `event.data` without casts.
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`, `steering/message`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
*/
type SessionEvent<T extends SessionEventType = SessionEventType> = {
[K in SessionEventType]: {
type: K
/** Monotonic sequence number within the session. */
seq: number
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of events that are provenance sources of this event
* (e.g. the `assistant/chunk` seqs that built an `assistant/message`,
* or the surface nodes shadowed by a compaction replace node). An
* `assistant/message` may carry a present empty array for a known empty
* provider stream; omission means unrecorded provenance.
*/
sourceEventSeqs?: number[]
/** How this event entered the surface; absent for non-surface events. */
surfaceOp?: SurfaceOp
} : object)
}[T]
```
十三种事件变体(`turn/start`、`turn/end`、`step/start`、`step/end`、`user/message`、`prompt/blocked`、`assistant/chunk`、`assistant/message`、`tool/call`、`tool/result`、`steering/message`、`todo/write`、`request/header`)、`deriveMessages()` 投影规则、`TurnTrigger`/`TurnEndReason` 原因以及轮次封闭不变量都在 **[session.md](session.md)** 中。日志如何持久化——`SessionPersistence` seam、JSONL/SQLite 后端、`session/flush` 检查点、崩溃恢复与 `SessionHeader`——则在 **[persistence.md](persistence.md)** 中。
<a id="the-agent-handle"></a>
## Agent 句柄
`Agent` 是每个插件UI、钩子、orchestrator面向编程的 surface。具体实现为 dsh-agent-loop 包内部细节;循环外没有任何组件依赖它。
源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
```ts type-equiv
/**
* Options for {@link Agent.followup}, {@link Agent.queue}, and {@link Agent.steer}.
* An omitted source attests direct human input as `{ kind: 'user' }` and may
* authorize policy consumers, so non-human producers must label their content.
*/
interface SendOptions {
source?: MessageSource
/**
* Model-facing contexts captured with this inbox item. A queued prompt exposes
* them through the default `agent/prompt-submit` allow decision, while steering
* records them directly at its next checkpoint.
*/
contexts?: HookContext[]
/** Opaque JSON state retained on the durable message but hidden from the model. */
meta?: JsonValue
}
```
```ts type-equiv
/** Options specific to durable synthetic context injection. */
interface InjectOptions {
/** Defaults to `{ kind: 'plugin', plugin: '' }`; non-human producers should identify themselves. */
source?: MessageSource
/** Opaque JSON state retained on the durable message but hidden from the model. */
meta?: JsonValue
}
```
高级接收形式会显式给出所有默认值,并禁止为注入附加上下文:
```ts type-equiv
/**
* Fully specified input for {@link Agent.send}. Unlike the intent-named
* helpers, this form applies no defaults: callers provide content, source,
* contexts, metadata (including explicit `undefined`), target, and wakeup.
* The union excludes attached contexts from non-waking next-step injection.
*/
type ResolvedAgentInput = {
content: ContentBlock[]
source: MessageSource
meta: JsonValue | undefined
} & (
| { target: 'next-turn'; wakeup: boolean; contexts: HookContext[] }
| { target: 'next-step'; wakeup: true; contexts: HookContext[] }
| { target: 'next-step'; wakeup: false; contexts: [] }
)
```
FIFO 投递方法返回不透明的 `AgentMessageId`,该 id 在同一条消息的各个 `agent/inbox/*` 事件中保持稳定。注入也返回 id但会绕过这些事件
```ts type-equiv
/**
* Opaque id assigned to one accepted agent input. FIFO inputs carry the same id
* on their `agent/inbox/*` events; injection bypasses those events.
*/
type AgentMessageId = Branded<'AgentMessageId'>
```
`agent/inbox/*` 实时事件承载一条已接收的消息;注入绕过两个 FIFO从不出现在这些事件中
```ts type-equiv
/**
* One accepted FIFO message, carried by the `agent/inbox/*` live events. `id`
* is the value returned by the accepting helper or {@link Agent.send},
* stable across this message's enqueue, dequeue, and discard events. Source
* defaults, when applicable, are already applied, so these are the exact values
* the item was accepted with.
* `steering` is true for an item drained between steps; otherwise it is claimed
* at a turn boundary. `SendOptions.meta` is intentionally omitted: it is durable
* model-hidden state that lands on the eventual `user/message`/
* `steering/message`, not live-event routing data.
*/
interface AgentMessage {
/** The id returned by the accepting helper or {@link Agent.send}. */
id: AgentMessageId
content: ContentBlock[]
source: MessageSource
contexts: HookContext[]
/** Whether the item joined the steering FIFO rather than the queued FIFO. */
steering: boolean
/** Whether the item wakes the driver or requests another step. */
wakeup: boolean
}
```
```ts type-equiv
/** Options for {@link Agent.cancel}. */
interface CancelOptions {
/**
* Preserve queued and steering inbox items instead of discarding them. The
* active turn is still aborted, but un-started and pending work survives for a
* later turn and no `agent/inbox/discard` fires.
*/
keepInbox?: boolean
}
```
```ts type-equiv
/** Stable runtime cause accepted by {@link Agent.cancel}. */
type AgentCancelCause =
| { readonly kind: 'user' }
| { readonly kind: 'parent' }
```
结构化 `Agent` 接口公开四个按意图命名的辅助方法,以及接受完全解析输入的方法。具体驱动器只需实现一次这套路由矩阵,每个辅助方法提供其固定路由与默认值。
```ts type-equiv
/** Public agent handle; its concrete implementation is internal to `@deepseek-ai/dsh-agent-loop`. */
interface Agent {
/** The single identity shared with {@link session}. */
readonly id: SessionId
/** The provider route and model this agent's requests use. */
readonly options: AgentOptions
/** The live session this agent drives; its log is the durable source of truth. */
readonly session: Session
/** The current lifecycle state, mirrored on every `agent/status` transition. */
readonly status: AgentStatus
/** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
readonly ctx: Context
/**
* Queue an ordinary message as its own FIFO-ordered turn and wake the driver.
* Content, resolved source, and attached contexts are detached, validated,
* and frozen together; invalid input throws synchronously before notification
* or enqueue.
* @param content - the prompt content blocks.
* @param options - source, attached contexts, and durable model-hidden meta.
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
*/
followup(content: ContentBlock[], options?: SendOptions): AgentMessageId
/**
* Queue an ordinary message without waking an idle driver. The item retains
* FIFO order and is claimed only after another input wakes the driver. A lone
* queued item leaves `whenIdle()` resolved.
* @param content - the prompt content blocks.
* @param options - source, attached contexts, and durable model-hidden meta.
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
*/
queue(content: ContentBlock[], options?: SendOptions): AgentMessageId
/**
* Submit steering into the running turn and request another step. An open turn
* records it at the next steering checkpoint before a request or continuation
* decision; policy may stop before another step. After turn close and its
* checkpoint, any remainder is queued for a later turn; terminal
* `agent/turn-stop`, cancellation, or disposal may discard it. Idle steering
* becomes a waking ordinary turn.
* @param content - the steering content blocks.
* @param options - source, attached contexts, and durable model-hidden meta.
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
*/
steer(content: ContentBlock[], options?: SendOptions): AgentMessageId
/**
* Append detached model-facing context without running the model. An open-turn
* injection joins at the current log position unless the current tool batch is
* executing; then it waits FIFO until that batch settles and drains before
* turn close even when interrupted. Idle injection uses a one-shot turn and
* durability checkpoint. Disposal awaits idle checkpoints; flush failures
* report through `agent/error`. An omitted source defaults to
* `{ kind: 'plugin', plugin: '' }`.
* @param content - the injected context content blocks.
* @param options - source and durable model-hidden meta.
* @returns the accepted injection's {@link AgentMessageId}; injection emits no `agent/inbox/*` events.
*/
inject(content: ContentBlock[], options?: InjectOptions): AgentMessageId
/**
* Accept one fully specified input through the same snapshot and routing path
* as the four intent-named helpers. `next-turn` targets the ordinary FIFO;
* `next-step`/wakeup targets steering (falling back to an ordinary waking turn
* while idle); and `next-step` without wakeup injects durable context without
* running the model. Every field is mandatory and no source or routing default
* is applied. Invalid input throws synchronously before notification, enqueue,
* or append.
* @param input - the resolved content, attribution, context, metadata, and routing facts.
* @returns the accepted input's {@link AgentMessageId}, carried by FIFO lifecycle events when applicable.
*/
send(input: ResolvedAgentInput): AgentMessageId
/**
* Clear queued and steering work — unless `keepInbox` — and abort the active
* turn. An effective call first emits `agent/cancel-requested` with the
* resolved typed cause. The first cause wins for the active turn, and
* `whenIdle()` resolves after cancellation reaches quiescence. Omitted cause
* means `{ kind: 'user' }`. Idle cancellation is a no-op and does not arm
* later work. The active turn snapshots and freezes the cause.
* @param cause - the stable caller intent carried by the current turn signal.
* @param options - cancellation options; `keepInbox` preserves pending work.
*/
cancel(cause?: AgentCancelCause, options?: CancelOptions): void
/** Resolve at idle quiescence; disposal waits for driver exit rather than only the status transition. */
whenIdle(): Promise<void>
}
```
`AgentStatus` 为 `'idle' | 'running' | 'disposed'``SessionId` 是品牌类型。`running` 描述整个驱动器的排空区间,可能跨越轮次关闭、其持久化检查点以及连续的排队轮次;它不能证明某个轮次仍然打开。`AgentOptions` 可合并扩展core 声明 `provider?` 与 `model?`(在 `agent/request` 后分发要求两者都存在。Persona 归 `dsh-system-prompt` 所有agent 作用域的 `deployment:persona` 可以遮蔽全局默认值。
cause 是由 TypeScript 强制约束的同进程输入。活跃的 `TurnCancellation` 持有者会把其判别字段复制到仅运行时的 `AbortSignal.reason`,并在发布 `turn/end` 前退役;冻结后的 `AbortSignal.reason` 仍可读取。`agentInterruptReasonOf(signal)` 无需查询环境中的 initiator 状态,即可识别 `user`、`parent` 与仅用于生命周期的 `disposed`。持久 `turn/end` 保留粗粒度 `{ kind: 'aborted' }` 结果;若需记录请求 provenance应使用单独的持久事件而不是让终态结果承担额外含义。
[事件分类](../architecture.md#event)拥有 `agent/*` 生命周期、检查点与 waterfall瀑布式事件契约。轮次和步骤边界是持久会话事件而不是 agent emit。
## 发起 Agent
`ctx.agents` 携带的进程本地 initiator 就是上面的确切 `Agent`,不是单独的 frame 或复制的标识。环境中存在该值既不能证明存活,也不代表授权;其生命周期与边界规则由 [initiator 作用域决策](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)规定。
## 拦截决策
每个 `agent/*` 拦截 waterfall 都返回一个小型、特定于 seam 的类型化联合——统一的 Decision 惯用形状([tools.md](tools.md) 中工具 seam 的 `PreToolDecision`/`PostToolDecision` 也采用相同形状。CC/Codex 钩子桥接层把其 `permissionDecision`/`decision`/`continue`/`additionalContext` 字段映射到这些联合上;原生插件则直接返回它们。提示词决策与工具后决策共享一种面向模型的上下文形状 `HookContext`,它必须携带 `source`(缺少 source 会默认成 `{kind:'user'}`,从而把插件上下文错标为用户提示词)。其中的 `content` 作为 user-role 输入逐字到达模型,而 JSON `meta` 持久保存插件状态但不向模型暴露。未指定放置方式或指定为 `separate` 时,上下文会成为一条注入的 `user/message`(来源类别为插件或 goal`prompt-prefix` 放置方式可用于提示词和 steering 收件箱附件,会在同一条消息中把上下文置于最终生效的请求之前。两种决策都携带 `additionalContexts[]`,使每一项保留各自的 provenance、元数据与放置方式。Continuation reason 则是 steering 消息,并有意使用更窄的 content/source 形状。
源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
```ts type-equiv
/** Model-facing context injected by a listener or atomically attached to one inbox message. */
interface HookContext {
content: ContentBlock[]
source: MessageSource
/**
* Model placement. Absent or `separate` records an independent injected
* `user/message`; `prompt-prefix` prepends this context and a stable
* request delimiter to the same user-role message as its attached prompt.
*/
placement?: 'separate' | 'prompt-prefix'
/** Opaque JSON state retained in the session event but hidden from the model. */
meta?: JsonValue
}
```
`agent/prompt-submit` 返回 `PromptDecision`(允许该轮次已领取的排队消息——可选地改写其 `content` 或附加 `additionalContexts`——或者记录 `prompt/blocked` 并以 `rejected` 结束这个零步骤轮次):
```ts type-equiv
/**
* Prompt interception result. `allow.content` replaces the prompt. Each
* `additionalContexts` entry follows its declared placement: separate context
* message by default, or a prefix inside the prompt's user-role message.
* `block` records a durable `prompt/blocked` and ends the claimed prompt's
* zero-step turn as rejected. An `allow` returned by a listener is
* authoritative: a listener wrapping `next()` preserves downstream `content`
* and `additionalContexts` unless it intentionally replaces them.
*/
type PromptDecision =
| { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: HookContext[] }
| { kind: 'block'; reason: string }
```
`agent/turn-continuation` 返回 `ContinuationDecision`(步骤有工具调用或注入了 steering 时,循环默认为 `continue`,否则为 `stop``continue` 的 `reason` 会记录为同一轮次中下一个步骤的 steering因此不携带上下文元数据——即类型化 `/goal` 模式):
```ts type-equiv
/** Turn continuation override; a continue reason is recorded as next-step steering in the same turn. */
type ContinuationDecision =
| { action: 'stop' }
| { action: 'continue'; reason?: { content: ContentBlock[]; source: MessageSource } }
```
`agent/request-error` 接收确切的原始 `RequestError`、其不可变 `LlmFailure`、在连续序列中已批准另一次请求的不可变失败列表、轮次信号以及 `next()`。恢复插件按 `failure.code` 路由,而不是按活跃错误的消息路由;每项策略只统计自身的 code一次成功请求会清空历史
```ts type-equiv
/** Model-request failure with an optional machine-routable provider code. */
type RequestError = Error & { code?: string }
```
它返回 `RequestErrorDecision``retry` 在恢复 listener 的持久变更之后打开一个带新编号的步骤,而 `fail` 在 `turn/end` 上保留结构化失败:
```ts type-equiv
/** Failed-request recovery decision; `retry` opens another numbered step while listeners delegate by calling `next()`. */
type RequestErrorDecision = { action: 'fail' } | { action: 'retry' }
```
`agent/post-step` 会在 assistant 输出、真实或合成的工具结果、缓冲上下文与 steering 持久化之后、`step/end` 之前被 await。被取消的工具批次在排空后携带 aborted signal 到达这里;其签名为 `(agent, turn, step, signal)`,可回放事实保留在会话日志中,而不是瞬态 payload 中。
`agent/turn-stop` 返回仅停止的 `ContinuationStop` 子集或 `undefined`。循环在折叠普通决策、其 reason 和待处理 steering 之后调用此串行检查点stop 是终态,会丢弃待处理的 steering。
```ts type-equiv
/**
* The terminal subset of {@link ContinuationDecision}. A listener on
* `agent/turn-stop` returns this to make the already-composed continuation
* outcome terminal; `undefined` abstains.
*/
type ContinuationStop = Extract<ContinuationDecision, { action: 'stop' }>
```
`agent/session-start` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart
```ts type-equiv
/** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */
type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
```
`agent/session-prefix` 在每个循环实例中组合一次 `Message[]`。深度冻结的结果被记录在请求 header 中,并前置于每次派生历史,使其成为会话稳定开场白的归属。恢复的实例会重新组合;会话中途的变更使用仅追加的上下文通道。该 waterfall 直接返回内容,因为它是贡献而非决策。
## `ToolDefinition`
唯一属于核心的流水线编写类型:每个已注册工具*是什么*——一个面向模型的 `ToolSchema` 加上一个 `execute` 函数,以及可选的最终内容回调与 UI 回调。工具作者很少手动构造它(`defineTool` DSL 会用类型化参数构建),但它是注册表持有、循环分发所经过的契约。
其完整字段、`defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` 类型化 schema DSL、`ToolExecution`/`ToolExecutionResult` waterfall 形状,以及工具展示 UI 词汇在 **[tools.md](tools.md)** 中。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
filesystem.md: 110c1fd428b15c5094f9dcc94050cad61c324373
filesystem.zh.md: aca450364c05c6f756c36fccc11be7246767f3a4

View File

@@ -1,5 +1,7 @@
# Filesystem
English | [中文](filesystem.zh.md)
The optional filesystem capability has four parts: [dsh-fs](../../packages/fs/fs) owns `ctx.fs` and atomic text operations with optional version guards, [dsh-fs-local](../../packages/fs/fs-local) implements local disk, [dsh-fs-policy](../../packages/fs/fs-policy) adds observed-state and freshness rules through events rather than a service, and [dsh-tool-fs](../../packages/fs/tool-fs) directly executes model-facing read/write/edit calls and renders windows. It is outside the agent-loop spine; alternate backends do not change policy or tool schemas.
The model is **additive, not subtractive**: `ctx.fs` alone is a complete, unconstrained text-storage seam (`write` unconditionally creates-or-overwrites, `edit` unconditionally replaces literal text). `dsh-fs-policy` is a plugin that *adds* policy on top by deciding the `fs/*` waterfalls; removing it leaves the bare provider rather than breaking the tool, because the tool is not method-coupled to the policy. A deployment that loads `dsh-tool-fs` is expected to also load `dsh-fs-policy` so the default behavior is read-before-write/edit.

View File

@@ -0,0 +1,259 @@
# 文件系统
[English](filesystem.md) | 中文
可选的文件系统能力由四个部分组成:[dsh-fs](../../packages/fs/fs) 拥有 `ctx.fs` 以及带可选版本守卫的原子文本操作;[dsh-fs-local](../../packages/fs/fs-local) 实现本地磁盘后端;[dsh-fs-policy](../../packages/fs/fs-policy) 通过事件(而非服务)添加观测状态与新鲜度规则;[dsh-tool-fs](../../packages/fs/tool-fs) 直接执行面向模型的 read/write/edit 调用并渲染窗口。它位于 agent loop智能体循环主干之外替换后端不会改变策略或工具 schema。
该模型是**加法式而非减法式**的:`ctx.fs` 本身就是一个完整、无约束的文本存储 seam`write` 无条件创建或覆盖,`edit` 无条件替换字面文本)。`dsh-fs-policy` 是一个插件,通过裁决 `fs/*` waterfall瀑布式事件在上层*叠加*策略;移除它只会暴露裸提供方,而不会破坏工具,因为工具与策略之间没有方法级耦合。加载了 `dsh-tool-fs` 的部署通常也应加载 `dsh-fs-policy`,使默认行为为「先读后写/编辑」。
提供方源码:[`packages/fs/fs/src/types.ts`](../../packages/fs/fs/src/types.ts) 与 [`packages/fs/fs/src/index.ts`](../../packages/fs/fs/src/index.ts)。策略源码:[`packages/fs/fs-policy/src/types.ts`](../../packages/fs/fs-policy/src/types.ts)。读取渲染源码:[`packages/fs/tool-fs/src/read-render.ts`](../../packages/fs/tool-fs/src/read-render.ts)。
## 目标标识与元数据(提供方 seam
每个操作首先将用户提供的路径解析为不透明的后端目标。消费方可以显示 `displayPath`,但禁止解析 `targetKey`(一个品牌化的不透明 id也不得假设它是本地绝对路径。
```ts type-equiv
/**
* A path resolved by a backend into a stable identity. `resolve()` produces
* this; every other operation takes it.
*/
interface FsTarget {
/** Opaque key for stale guards and target lookup. */
targetKey: FsTargetKey
/**
* Path for model/UI-facing output. May be a local absolute path,
* workspace-relative path, or remote URI depending on the backend.
*/
displayPath: string
}
```
后端拥有文件版本 token即 write/edit 所守卫的新鲜度 token。策略插件存储它们以进行陈旧检查消费方不解释其内容。两个 id 都是品牌化的不透明字符串。
```ts type-equiv
/**
* Opaque key for stale guards and target lookup. The local backend uses a
* realpath-like string; a remote backend might use a workspace URI or file id.
* Consumers MUST NOT parse it or assume it is a local absolute path.
*/
type FsTargetKey = Branded<'FsTargetKey'>
```
```ts type-equiv
/**
* Opaque file-version token — the freshness token a write/edit guards against.
* The local backend derives it from high-resolution stat identity and freshness
* fields; a remote backend might use a revision id. The policy layer records it
* for stale checks; consumers may display related metadata but MUST NOT
* interpret this token.
*/
type FsVersion = Branded<'FsVersion'>
```
`stat` 返回元数据(从不返回内容),目标不存在时返回 `undefined`。`type` 让工具在读取前拒绝目录或特殊文件;`size` 让工具无需通过失败探测即可选择 `readText` 还是 `streamText`。
```ts type-equiv
/**
* Metadata about a target — what {@link FileSystem.stat} returns. Lets the
* policy layer reject directories/special files before reading and choose
* `readText` vs `streamText` from `size` without probing by failure. `version`
* is the freshness token. `undefined` from `stat` means the target is absent.
*/
interface FsInfo {
/** Opaque freshness token of the target right now. */
version: FsVersion
/** Whether the target is a regular file, a directory, or something else. */
type: 'file' | 'directory' | 'other'
/** Byte size of a regular file, when the backend can report it. */
size?: number
}
```
`lstat` 是路径层级、不跟随链接的元数据原语。它接收路径而不是 `FsTarget`,因为 `resolve` 会有意跟随 symlink 以产生稳定标识;需要检查信任边界的消费方可以先调用 `lstat`,在解析前拒绝 `symlink`。
```ts type-equiv
/**
* Metadata about a path without following the final path component when it is a
* symbolic link. Unlike {@link FsInfo}, this path-level probe can report
* `symlink` so consumers with trust-boundary rules can reject repository-owned
* links before resolving a target.
*/
interface FsPathInfo {
/** Opaque freshness token of the path entry right now. */
version: FsVersion
/** Whether the path entry is a regular file, directory, symlink, or other. */
type: 'file' | 'directory' | 'symlink' | 'other'
/** Byte size of the path entry, when the backend can report it. */
size?: number
}
```
`listDir` 按稳定的名称顺序返回直接子条目。每个条目携带子项的 basename、类型、已解析目标以及后端能报告时的廉价元数据。它禁止读取文件内容因此 `size` 仅用于普通文件,`version` 来自元数据。已损坏或已消失的子项可以作为 `other` 返回且不带元数据;列出或解析子项元数据时的权限或后端 I/O 失败会以 `FS_PERMISSION_DENIED` 或 `FS_IO_ERROR` 使整个列表操作失败。
```ts type-equiv
/**
* One direct child returned by {@link FileSystem.listDir}. Listing returns
* metadata and resolved targets only; it must not read file contents.
*/
interface FsDirEntry {
/** Basename of the child inside the listed directory. */
name: string
/** Whether the child is a regular file, a directory, or something else. */
type: 'file' | 'directory' | 'other'
/** Resolved child target for follow-up operations. */
target: FsTarget
/** Opaque freshness token when the backend can report metadata cheaply. */
version?: FsVersion
/** Byte size of a regular file, when the backend can report it. */
size?: number
}
```
## 写入与编辑守卫(提供方 seam
`writeText` 和 `editText` 的版本守卫都是可选的:省略它执行无条件(裸提供方)变更,提供它则启用守卫。`writeText` 的守卫是 `FsWriteIntent``createIfAbsent` 在目标缺失时创建,目标已存在时以 `FS_NOT_OBSERVED` 拒绝;`replaceIfVersion` 仅在目标存在且版本匹配时替换,否则报 `FS_STALE_VERSION`。省略 `expected` 则无条件创建或覆盖。联合类型本身只包含两种有守卫的意图;「无守卫」通过省略表达,因此 write 和 edit 共享同一个对称的 `expected?` 形状。
```ts type-equiv
/**
* Guarded write intent. `createIfAbsent` rejects an existing target with
* `FS_NOT_OBSERVED`; `replaceIfVersion` rejects absence or mismatch with
* `FS_STALE_VERSION`. Omitting the intent from `writeText` means unconditional
* create-or-overwrite, not a third union arm.
*/
type FsWriteIntent =
| { kind: 'createIfAbsent' }
| { kind: 'replaceIfVersion'; version: FsVersion }
```
```ts type-equiv
/** Outcome of a full-file write. */
interface FsWriteOutcome {
/** Whether the write created a new file or replaced an existing one. */
operation: 'create' | 'update'
/** Opaque version of the file after the write. */
version: FsVersion
/**
* The file's content BEFORE the write, or `null` when the file did not exist
* (a create) or was undiffable (binary/non-UTF-8). LF-normalized storage text
* (the diff basis), never a diff — a consumer computes the result-time
* contextual diff from `before`/`after` when `before` is present, else falls
* back to a whole-file diff.
*/
before: string | null
/** The file's content AFTER the write, LF-normalized to share `before`'s diff basis. */
after: string
}
```
`editText` 是提供方级别的变更操作,而非在别处组合的 `read` 加 `write`。带守卫时,它在字面匹配之前先验证预期版本(因此对陈旧内容的编辑报 `FS_STALE_VERSION`,而非对更新内容的匹配失败);不带守卫时,它编辑当前内容。无论哪种路径,它都应用替换并原子写入——将匹配、行尾处理、陈旧检查和原子替换保持在一个变更临界区内——目标缺失时两条路径都报 `FS_STALE_VERSION`。
```ts type-equiv
/** A literal-replacement edit request. */
interface FsEditRequest {
/** Literal non-empty text to replace. Must match exactly (after line-ending normalization). */
oldString: string
/** Literal replacement text. An empty string deletes the matched text. */
newString: string
/** Replace every match instead of requiring exactly one. */
replaceAll: boolean
}
```
```ts type-equiv
/** Outcome of a literal edit. */
interface FsEditOutcome {
/** Opaque version of the file after the edit. */
version: FsVersion
/**
* The file's content BEFORE the edit. Raw storage text (LF-normalized by the
* backend), never a diff — a consumer computes the result-time contextual diff
* (the applied hunk with context) from `before`/`after`.
*/
before: string
/** The file's content AFTER the edit. */
after: string
}
```
## fs 策略事件(提供方 seam 词汇)
`dsh-fs` 拥有三个事件,由工具分发、策略插件监听,使发射方(`dsh-tool-fs`)与监听方(`dsh-fs-policy`)共享词汇,而发射方无需依赖策略插件。它们只携带 `dsh-fs` 词汇加一个不透明的 `object` actor不含面向模型的概念也不含 agent/会话所有者结构。
`fs/write-intent` 与 `fs/edit-intent` 是**单槽决策 waterfall**:工具分发时附带一个默认 thunk返回 `undefined`,即裸提供方),监听方完全决策而不调用 `next()`。该槽按注册顺序先到先得——由策略插件占据是部署约定,而非强制不变式。`fs/observed` 是一个即发即弃的记录事件,通过普通 `ctx.emit` 分发;其监听方必须是同步的、仅产生副作用,因为工具不守卫该 emit——抛异常的监听方会在一次已成功的变更上表现为工具的 `isError` 结果。生成的目录在 [events.md](../cordis-catalog/events.md) 中展示确切签名。
## 执行上下文(策略插件)
策略插件只需要足够的执行上下文,通过收窄 `fs/*` 事件携带的不透明 `object` actor 来推导观测状态的所有者。`ToolExecution` 满足此形状,因此 `dsh-tool-fs` 将其执行对象作为 actor 直接传递,而无需让 `dsh-fs-policy` 导入工具、agent 或会话包package
```ts type-equiv
/**
* Minimal structural view of a tool execution the policy plugin needs to derive
* an observed-state owner. `@deepseek-ai/dsh-tools`' `ToolExecution` satisfies
* this shape, so the tool passes its `exec` straight through as the opaque
* `object` actor on the `fs/*` events; this plugin narrows that actor to this
* shape without importing `dsh-tools`, `dsh-agent`, or `dsh-session`.
*
* The owner is `agent.session` when present. It is treated as an opaque object
* identity (a `WeakMap` key); this package never reads any of its fields.
*/
interface FsPolicyExec {
/** The agent on whose behalf the call runs, when there is one. */
agent?: {
/** The session that owns observed-file state, used as an opaque key. */
session?: object
}
}
```
## 读取结果(消费方 / 读取渲染)
文本读取受行窗口、字节上限和后端限制约束。达到字节上限后,扫描仍会继续,但不再保留更多行,因此 `totalLines` 仍为精确值。面向模型的 `read` 工具渲染的结果纯粹是展示性的;不存在 `full`/`partial` 视图区分——授权基于新鲜度(工具直接用 stat 的版本 emit `fs/observed`),因此任何窗口化读取在文件未变时都能授权后续的 write/edit。读取窗口化与此结果形状位于 `dsh-tool-fs`(拥有读取操作的执行器)中,而非策略插件中。
```ts type-equiv
/** Outcome of a bounded text read — what {@link formatReadOutput} renders. */
interface FileReadOutcome {
/** 1-based first line requested. */
offset: number
/** Returned lines, already numbered. */
lines: FileTextLine[]
/** Exact total line count in the file. */
totalLines: number
/** Whether selected output hit the byte cap. */
truncatedByBytes?: true
}
```
## 已观测文件状态(策略插件)
已观测状态是 `dsh-fs-policy` 插件内部持有的 `WeakMap<owner, Map<targetKey, { version }>>`。**当且仅当**所有者已读取、写入或编辑过该目标时(每次成功都 emit `fs/observed`),条目才存在,因此其存在本身就是先前观测的记录——没有单独的 `hasRead` 标志,也没有视图区分。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。成功的 read/write/edit 会刷新该所有者对应的已记录版本dispose资源释放时丢弃全部数据HMR热模块替换安全
## 错误分类体系(提供方 seam
文件系统故障使用稳定的 `FsErrorCode` 字符串,由 `FsError``HarnessError`)携带。工具注册表在错误结果上保留 `{ name, code }`,使重试、权限和 UI 层可以按 code 分支而无需解析文本。
```ts type-equiv
/**
* Stable, machine-routable codes for filesystem failures. Carried on
* {@link FsError}; the tool registry surfaces `{ name, code }` on `isError`
* results so retry/permission/UI layers can branch without parsing messages.
*/
type FsErrorCode =
| 'FS_NOT_FOUND'
| 'FS_NOT_DIRECTORY'
| 'FS_NOT_TEXT'
| 'FS_NOT_REGULAR_FILE'
| 'FS_PERMISSION_DENIED'
| 'FS_SANDBOX_DENIED'
| 'FS_IO_ERROR'
| 'FS_STALE_VERSION'
| 'FS_NOT_OBSERVED'
| 'FS_AMBIGUOUS_EDIT'
| 'FS_EDIT_NOT_FOUND'
| 'FS_ABORTED'
```
目录列表使用 `FS_NOT_DIRECTORY`、`FS_PERMISSION_DENIED` 与 `FS_IO_ERROR` 区分已存在但并非目录的目标、被拒绝的列表操作和意外的后端 I/O 失败。`FS_SANDBOX_DENIED` 是强制执行沙箱的后端(`dsh-fs-sandbox`)所作的策略拒绝——模式边界拒绝了写入/编辑——与 `FS_PERMISSION_DENIED`(宿主内核拒绝)不同。`FS_NOT_OBSERVED` 表示策略插件没有此所有者的先前观察记录(或 `createIfAbsent` 遇到了现有文件)。`FS_STALE_VERSION` 表示后端版本不再与观察到的版本匹配(或编辑操作遇到缺失目标)。新鲜度授权没有部分/完整之分,因此不存在 `FS_PARTIAL_OBSERVATION`。
## 服务与插件
`FileSystem``ctx.fs`abstract拥有提供方原语`resolve`、`stat`、`lstat`、`readText`、`streamText`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:对写入/编辑意图 waterfall 作出决策(提供 `createIfAbsent`/`replaceIfVersion`/`{ version }`,或抛出 `FS_NOT_OBSERVED`),并在 `fs/observed` 上记录。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall并 emit 记录事件。生成的 wiring 目录在 [services.md](../cordis-catalog/services.md#ctxfs--filesystem-abstract-seam) 中展示确切的 `ctx.fs` 签名。

View File

@@ -69,7 +69,7 @@ interface GoalView extends GoalSnapshot {
## Durable changes
Every mutation is a `context/message` whose metadata is either a complete snapshot or a clear tombstone. The version, metadata, goal source, and verbatim rendered content form one replay invariant.
Every mutation is a round-zero goal-sourced `user/message` whose metadata is either a complete snapshot or a clear tombstone. The version, metadata, goal source, and verbatim rendered content form one replay invariant.
```ts type-equiv
/** Full-snapshot goal mutation retained in a model-visible context event. */

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
llm-streaming.md: 2917d9796b32956f11ed5703f1f34c7510d0526c
llm-streaming.zh.md: 1bf7b3c226b5878deff3252396d96c2e97bf2982

View File

@@ -1,5 +1,7 @@
# LLM Streaming
English | [中文](llm-streaming.zh.md)
The wire-level streaming vocabulary of [dsh-llm](../../packages/llm/llm). [core.md](core.md) introduces `StreamChunk`, `Message`, and `ContentBlock`; this page owns the full chunk protocol, the adapter contract every adapter must obey, and the shared assembler.
Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)

View File

@@ -0,0 +1,228 @@
# LLM大语言模型流式输出
[English](llm-streaming.md) | 中文
[dsh-llm](../../packages/llm/llm) 的协议格式wire format级流式输出词汇。[core.md](core.md) 介绍了 `StreamChunk``Message``ContentBlock`本页拥有完整的分片协议、每个适配器必须遵守的适配器契约adapter contract以及共享的 assembler。
源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
## `StreamChunk`:原始协议
一个流式响应交错包含多种类型的块文本、推理reasoning、多个工具调用`index` 将每个 delta 关联到其所属块;`block-end` 携带完整组装好的 `ContentBlock`,消费方无需自行重新组装 delta。这是一个**封闭的**可辨识联合类型:对 `type``switch``assertNever` 结尾,因此新增变体会在每个必须处理它的消费方处触发编译错误。
```ts type-equiv
/**
* Raw streaming protocol emitted by adapters.
* Block indexes correlate interleaved deltas, and `block-end` carries the
* assembled block. Adapters emit usage before the terminal finish and nothing
* afterward; tool arguments remain raw JSON strings. Failures either throw or
* end with `error`/`aborted`, and consumers must handle both paths.
*/
type StreamChunk =
| { type: 'block-start'; index: number; blockType: ContentBlockType }
| { type: 'text-delta'; index: number; text: string }
| { type: 'reasoning-delta'; index: number; text: string }
| { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string }
| { type: 'block-end'; index: number; block: ContentBlock }
| { type: 'usage'; usage: TokenUsage }
| {
type: 'finish'
reason: FinishReason
/** Adapter-private lossless-JSON state for replaying a successful response. */
replayState?: unknown
}
```
## `LlmFailure`
每个抛出的失败或最终适配器的带内失败都会规范化为一种可序列化、提供方无关的 payload。`providerRetryAfterMs` 是经校验、由提供方请求的正数延迟,而不是重试决策;`ProviderRequestId` 是用于诊断的不透明品牌字符串。
```ts type-equiv
/** Serializable provider-boundary facts; policy decides whether they are retryable. */
interface LlmFailure {
/** Human-readable provider or transport failure. */
readonly message: string
/** Stable provider-neutral machine-routing code. */
readonly code: string
/** HTTP status observed at the provider boundary, when available. */
readonly status?: number
/** Provider-requested delay in milliseconds, when valid and available. */
readonly providerRetryAfterMs?: number
/** Opaque provider-issued request identifier for diagnostics. */
readonly requestId?: ProviderRequestId
}
```
## 适配器契约
每个适配器必须遵守以下规则,每个消费方可以依赖它们:
- **`usage` 在 `finish` 之前,`finish` 之后不再有任何分片。** 将两者都推迟到提供方的流结束标记,这样尾部的 usage-only 分片就不会违反顺序。
- **工具调用的 `arguments` 全程保持原始 JSON 字符串。** 部分片段通过 `argumentsDelta` 流式传输;如果提供方返回的是已解析的对象,适配器在 `block-end` 时重新序列化为字符串。
- **两条受支持的错误路径,一种事实形状。** 失败可以从 `stream()` 抛出(传输/协议错误),**或者**以 `finish {kind:'error'|'aborted', failure}` 结束流(无法在流中途抛异常的适配器用它表示提供方带内错误)。`LlmError.failure` 携带同一个 `LlmFailure`。最终适配器边界保留被抛出的确切 `Error` 对象并将不可变事实关联到该调用agent loop智能体循环关闭失败的步骤再把错误、事实与不可变的先前已重试事实提供给 `agent/request-error`。若未恢复,结构化失败会成为轮次错误,并且该次尝试不会提交正常 assistant 消息或工具副作用。
- **一次适配器调用就是一次提供方尝试。** 适配器禁用库重试。agent 层恢复会打开另一个持久、带编号的步骤;直接调用 `ctx.llm.stream()` 的调用方仍然只尝试一次。
- **提供方停顿在传输层受到时限约束。** 两个已交付的远程适配器都暴露正数且有限的 `streamIdleTimeoutMs`默认五分钟。watchdog 只在 iterator `next()` 尚未完成时启动,整个请求使用同一个稳定 signal把自身到期映射为 `TIMEOUT`,并把更早发生的调用方中止保留为 `ABORTED`。
- **上下文溢出只有一个规范 code。** 两个 DeepSeek 适配器都通过 `isContextWindowExceededError()` 对提供方的显式细节分类并暴露 `CONTEXT_WINDOW_EXCEEDED`,无论失败以抛出的 HTTP `LlmError` 还是带内 finish error 到达。消费方按 code 路由,绝不依赖提供方文本。
- **每个提供方 HTTP 请求都携带应用归属头。** 适配器发送 `attributionHeaders()`(见下文)作为 `User-Agent` 基线并通过协议级测试加以证明mock 服务器断言收到的 header或对基于库的适配器使用库的 header 钩子)。
- **回放状态归适配器所有。** 成功的 `finish` 可以携带重建提供方原生响应所需的无损 JSON 状态。除非 `agent/step-result` listener 改写了内容,否则循环会将其与组装后的 assistant 消息一起存储。后续请求中,仅当历史提供方与目标提供方当前注册到完全相同的适配器实例时,`LlmService` 才会传递该状态。该适配器负责校验状态并拥有所有跨模型或跨提供方转换;其他适配器只会收到提供方无关的内容与 provenance不会收到私有状态。
该契约由两个有意保持独立的实现锁定:`dsh-llm-deepseek`(手写 fetch/SSEServer-Sent Events和 `dsh-llm-pi-ai`(通过 `@earendil-works/pi-ai` 实现的通用多提供方适配器)。基于库的适配器覆盖 finish 分片错误路径,而传输边界测试证明每个空闲 watchdog 都会停止其实际请求。
## `ResolvedRetryPolicy`
提供方配置会在路由注册前解析为不可变的可辨识联合类型。normal 模式包含 `mode: 'normal'`、有限的 `maxRetries`、`retryableCodes`,以及必填的 `initialDelayMs`、`maxDelayMs` 和 `jitterRatio`always 模式包含 `mode: 'always'` 和相同的必填退避字段,但不含有限上限。`LlmService.providerRetryPolicy(provider)` 返回捕获的值;适配器未提供策略时,该方法会补上 normal 默认值。可选输入形状由[生成的配置目录](../config-catalog.md)定义。
## `AppIdentity`:应用归属
每个适配器都会向提供方发送的静态公开应用标识([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts))。`attributionHeaders(identity?)` 只把它映射到标准 `User-Agent` header该契约有意不支持 OpenRouter 特有的应用归属 header。默认 `APP_IDENTITY` 从包package manifest元数据清单获取版本每个字段都是公开产品事实——不含 secret、路径、会话 id 或逐用户标识,且任何逐请求信息都不得影响这些值。设计理由见[强制 `User-Agent` 归属](../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。
```ts type-equiv
/**
* Static public application identity sent to LLM providers.
*
* Every field is a public product fact, safe on every request: no secrets,
* local paths, session ids, prompt text, or per-user identifiers belong here,
* and nothing per-request may influence the values.
*/
interface AppIdentity {
/** `User-Agent` product token (lowercase, hyphenated). */
product: string
/** Product version; sourced from package metadata, never hand-copied. */
version: string
/** Public home URL of the app, used as the `User-Agent` comment. */
url: string
}
```
## `TokenUsage`
逐调用 token 记账。各计数**互不重叠**`inputTokens` 只包含未缓存输入;缓存输入单独报告,计费输入是三者之和。若提供方把缓存命中折入单一提示词总数(如 DeepSeek 的 `prompt_tokens`),适配器会再将其扣除。`reasoningTokens` 存在时只是信息性细节,已经包含在 `outputTokens` 中;汇总时不得重复相加。
```ts type-equiv
/**
* Token accounting for one model call (cache fields are optional).
*
* Counts are DISJOINT: `inputTokens` is uncached input only; cached input is
* reported separately as `cacheReadTokens`/`cacheWriteTokens` (billed input =
* sum of the three). Adapters whose providers fold cache hits into a total
* prompt count (DeepSeek's `prompt_tokens`) subtract them out.
*/
interface TokenUsage {
inputTokens: number
outputTokens: number
cacheReadTokens?: number
cacheWriteTokens?: number
reasoningTokens?: number
}
```
## `BlockAssembler`
`BlockAssembler`[`packages/llm/llm/src/assembler.ts`](../../packages/llm/llm/src/assembler.ts))是唯一的共享实现,负责把 `StreamChunk` 流折叠回 `ContentBlock`、usage、结束原因与回放状态。循环在记录原始分片的同时把同一批分片送入 assembler再将组装后的 assistant 内容连同其提供方/模型 provenance 一起存储。需要组装结果、又不想重新实现 fold 的消费方使用它。
```ts public-api
/**
* Incrementally assembles raw {@link StreamChunk}s into complete
* {@link ContentBlock}s and a final assistant {@link Message}.
*
* The agent loop feeds it while logging raw chunks for replay fidelity, then
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends.
*
* Tolerant of delta-only protocols (no block-start/end); deltas arriving for
* an index already closed by `block-end` are ignored (malformed stream) so a
* misbehaving adapter cannot grow memory or corrupt a completed block.
*/
declare class BlockAssembler {
/**
* Feed one chunk into the assembly state.
* @param chunk - the next raw chunk, in stream order.
*/
push(chunk: StreamChunk): void;
/**
* Assemble all blocks seen so far, in stream order.
* @returns one block per seen index; an open block assembles from its
* accumulated deltas (an unknown block type never closed by `block-end` throws).
*/
blocks(): ContentBlock[];
/** Usage from the `usage` chunk; undefined until one arrives. */
get usage(): TokenUsage | undefined;
/** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
get finish(): FinishReason;
/** Adapter-private replay state from the terminal finish chunk, if any. */
get replayState(): unknown;
/**
* The assembled assistant message.
* @returns an assistant-role message over `blocks()` (same open-block assembly rules).
*/
message(): Message;
}
```
## seam
`LlmAdapter` 是提供方 seam创建子类、实现 `stream()`,再用 `ctx.llm.registerAdapter(providers, adapter)` 注册一个适配器实例。`GenerateOptions.provider` 选择已注册适配器;`GenerateOptions.model` 会传给该适配器,无需在生命周期启动时注册。重复提供方路由会原子失败。可选的 `providerRetryPolicy()` 会按路由捕获并采用 normal 默认值,而 `providerInfo()` 与异步 `listModels()` 方法为 `LlmService.listProviders()` / `listModels()` 提供分离的 selector 元数据。该目录仅供参考,不是请求白名单:适配器仍是权威,并可接受未列出的模型 id。单独的 `resolveModelContext()` 查询会暴露确切路由上对正确性敏感的容量信息,但不会让目录成员关系具有权威性;缺失表示元数据未知,而不是路由无效。适配器查找发生在 `llm/stream` waterfall瀑布式事件的终端 continuation因此 listener 可以在查找前短路调用,或路由一个可变的一次性请求。`block-start` / `block-end` 的 `index` 关联与 assembler 共同意味着适配器只需 emit 格式正确的分片——块重组不是每个适配器各自的问题。消费方 surface`ctx.llm.stream()`)与 `llm/stream` waterfall 见 [architecture.md § 内容块与流式传输](../architecture.md#content-blocks-and-streaming-dsh-llm)。
```ts public-api
/**
* Provider-wire adapter for the harness message and stream vocabulary. Register implementations
* with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
* `attributionHeaders()`; prove that at the wire or library header-hook boundary. The hand-rolled
* DeepSeek and pi-ai adapters intentionally exercise this contract through different internals.
*/
declare abstract class LlmAdapter {
/**
* Describe one provider route owned by this adapter.
* @param provider - a route passed to `registerAdapter()` for this instance.
* @returns detached display metadata whose id must equal `provider`.
*/
providerInfo(provider: string): LlmProviderInfo;
/**
* Return the provider-owned retry policy captured with this route.
* @param _provider - a route passed to `registerAdapter()` for this instance.
* @returns a resolved policy, or `undefined` to use the normal defaults.
*/
providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined;
/**
* List models this adapter can currently advertise for one owned provider.
* The result is advisory: an adapter may accept unlisted model ids, and
* consumers must not turn absence into request rejection.
* @param _provider - one provider route owned by this adapter.
* @returns discoverable models in adapter-preferred order.
*/
listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
/**
* Resolve context capacity for one model accepted by this adapter. Absence
* means the adapter does not know the capacity, not that routing is invalid.
* @param _provider - one provider route owned by this adapter.
* @param _model - exact model id passed to {@link GenerateOptions.model}.
* @returns provider-owned context metadata, or `undefined` when unavailable.
*/
resolveModelContext(
_provider: string,
_model: string,
): Promise<LlmModelContext | undefined>;
/**
* Stream one model call as raw chunks. The only required method.
* @param options - the fully-assembled request; implementations must honor `options.signal`.
* @returns the chunk stream, obeying the adapter contract documented on `StreamChunk`.
*/
abstract stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
}
```
`ContentBlockType``index` 关联块所携带的键集合)派生自 `ContentBlockMap`
```ts type-equiv
/**
* Merge-extensible content blocks keyed by `type`. New core blocks must land
* with adapter, UI, and compaction support.
*/
interface ContentBlockMap {
'text': TextBlock
'reasoning': ReasoningBlock
'tool-call': ToolCallBlock
'tool-result': ToolResultBlock
}
```
块接口详见 [core.md § Content blocks and messages](core.md#content-blocks-and-messages)。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
persistence.md: dc497fd85f44660c0a981579351b5cfbe0040a4d
persistence.zh.md: 5236f4fe2ba8ad1be7e74bffafebfea19014d7aa

View File

@@ -1,5 +1,7 @@
# Session Persistence
English | [中文](persistence.zh.md)
The **durability seam** for the event log. [session.md](session.md) describes the in-memory `Session` — the append-only `SessionEvent` log that is the source of truth. This page describes how that log is made durable: the abstract `SessionPersistence` service, its backends, the flush checkpoint, crash recovery, and the metadata header that travels alongside the log. The event vocabulary the log carries is enumerated, member by member, in the generated [persistence log event catalog](../persistence-catalog.md).
The seam is a textbook [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): one abstract service ([dsh-session-persistence](../../packages/session-persistence/session-persistence), `ctx.sessionPersistence`) defining locate/create/append, crash-repairing load, non-mutating inspect, and lightweight list/snapshot observation over the existing `SessionEvent`**no parallel persisted type** — and two interchangeable backends that pass the same `runPersistenceContract` suite. See the [session-persistence Agent Note](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md).
@@ -53,7 +55,7 @@ interface SessionHeader {
readonly version: number
/** The session's id (mirrors the {@link Session}'s id). */
readonly id: SessionId
/** Unix epoch milliseconds when the session was created. */
/** Non-negative safe-integer Unix epoch milliseconds when the session was created. */
readonly createdAt: number
/** Absolute working directory the session was created in (if any). */
readonly cwd?: string

View File

@@ -0,0 +1,134 @@
# 会话持久化
[English](persistence.md) | 中文
事件日志的**持久性 seam**。[session.md](session.md) 描述了内存中的 `Session`:仅追加的 `SessionEvent` 日志即为真源。本页描述如何使该日志持久化:抽象的 `SessionPersistence` 服务、它的后端、flush 检查点、崩溃恢复,以及随日志一同存储的元数据头。日志承载的事件词汇在生成的[持久化日志事件目录](../persistence-catalog.md)中逐项列举。
该 seam 是典型的[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):一个抽象服务([dsh-session-persistence](../../packages/session-persistence/session-persistence)`ctx.sessionPersistence`)在现有 `SessionEvent` 上定义 locate/create/append、会执行崩溃修复的 load、不会修改数据的 inspect以及轻量的 list/snapshot 观察——**没有平行的持久化类型**——以及两个可互换、通过同一套 `runPersistenceContract` 的后端。见 [session-persistence Agent Noteagent 决策记录)](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)。
## flush 检查点
`session/event` 是一个*同步*通知;持久化插件会将事件复制到逐会话控制器,并立即启动写入而不阻塞生产方。并发事件会加入当前批次;在该批次写入期间接纳的事件会触发后续批次。`session/flush` 会等待当前与待处理批次全部清空,因此循环仍将其用作在领取下一个普通轮次之前的顺序与错误观察检查点。立即写入被拒绝时会保留对应事件;显式 flush 会重试这些事件,并通过 `agent/error` 和 logger 报告失败绝不会把失败记录成已关闭轮次之后的会话事件。dispose资源释放会执行同样的最终排空。
## 崩溃恢复保留被中断的轮次
后端重新加载一个在轮次中途崩溃的日志时,会发现一个已打开的 `turn/start` 却没有 `turn/end`。它**不会**截断日志:在长周期任务中,单个轮次可能非常庞大(许多步骤、大量工具输出),而这些事件在崩溃前已被持久追加。后端改为用一个合成的 `turn/end { reason: { kind: 'interrupted' } }` 关闭这个遗留轮次,保持日志平衡与轮次闭合不变式。`interrupted` 是唯一一个不由循环发出的 `TurnEndReason`(见 [session.md](session.md#why-a-turn-ended-turnendreasonmap))。
修复仅适用于冷会话。对于活跃 id`SessionPersistence.load(id)` 会对内存日志拍摄快照,等待该快照完成持久化,并且只在日志平衡时连同已存储的 header 返回;若活跃轮次仍未闭合,则拒绝操作,而不是添加合成的中断边界。由协调器管理的冷加载会在后端读取和修复写入期间占用该 id因此并发发布同 id 的活跃会话会被拒绝并回滚。HMR 也会接管活跃前缀,而不会关闭其中正在进行的轮次。
`SessionPersistence.inspect(id)` 是恢复机制面向观察方的对等操作:它返回已存储有效前缀的独立副本,不截断不完整记录、不添加中断结束事件,也不发布写入状态。同 id 串行化确保它与后端写入保持一致。派生读取模型使用 `inspect`,绝不使用 `load`,因此即使活跃所有权并发建立,观察已落检查点但仍未闭合的轮次也不会修改日志。
## `SessionLocation`——可选的逐会话产物目标
`SessionPersistence.locate(meta)` 会同步解析一个归后端所有的独立产物,而不会读取、创建或 flush 它。JSONL 返回其绝对目标路径SQLite 因各会话共享一个数据库而返回 `undefined`。因此,返回的路径可能指向尚不存在、或还不包含当前尚未 flush 的轮次;它是位置提示,不是授权或新鲜度保证。
```ts type-equiv
/**
* A backend-resolved, per-session local artifact location. The path is an
* absolute target path and can name an artifact that has not materialized yet.
* Consumers must treat it as a location hint, never as an authorization token.
*/
interface SessionLocation {
/** Backend-specific artifact kind, for example `jsonl`. */
readonly kind: string
/** Absolute path to this session's backend-owned artifact. */
readonly path: string
}
```
## `SessionHeader`:日志旁的元数据
每个会话的元数据与事件日志**分开**存储格式版本、cwd、血统与 seed 边界是存储层关注点而非对话事件,因此不进入 `SessionEventMap`,也不会到达 `deriveMessages()`。header 通过 `session.header` 附加到 `Session` 上。
源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
```ts type-equiv
/**
* Immutable validated storage metadata, kept outside the conversation event log.
*/
interface SessionHeader {
/**
* On-disk format version, stamped from {@link SESSION_FORMAT_VERSION} when the
* session is created. A persistence backend rejects any other version on load
* (no migration — see the constant).
*/
readonly version: number
/** The session's id (mirrors the {@link Session}'s id). */
readonly id: SessionId
/** Non-negative safe-integer Unix epoch milliseconds when the session was created. */
readonly createdAt: number
/** Absolute working directory the session was created in (if any). */
readonly cwd?: string
/** The session this one was forked from (seed lineage), if any. */
readonly parentSession?: SessionId
/**
* How many leading events were inherited through a seed. Persisting this
* boundary lets resume and replay distinguish parent history from child work.
*/
readonly seedLength?: number
/**
* Delegation depth: absent (zero) for a top-level session, parent depth + 1
* for a subagent child. Persisted so a recursion budget survives restart and
* resume — a runtime-only depth would reset a resumed child to top-level.
*/
readonly delegationDepth?: number
}
```
## `CreateSessionOptions`seed 与元数据
通过 store 创建 `Session` 时会接收 `seed`(回放/fork 现有事件日志)与 `meta`store 折叠进 `SessionHeader` 的存储层字段。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、`delegationDepth`,以及——仅在重建已持久化会话时——需要保留的原始 `createdAt`。
```ts type-equiv
/**
* Options for creating a {@link Session} via the store. `seed` replays/forks
* an existing event log; `meta` carries the caller-supplied storage fields the
* store folds into a {@link SessionHeader}.
*/
interface CreateSessionOptions {
/** Events to seed the new session with (replay/fork). */
readonly seed?: readonly SessionEvent[]
/**
* Storage metadata read once before publication. `seedLength` is explicit
* because a resumed seed contains the full stored log, not only its inherited prefix.
*/
readonly meta?: {
readonly cwd?: string
readonly parentSession?: SessionId
readonly createdAt?: number
readonly seedLength?: number
readonly delegationDepth?: number
}
}
```
因此,回放/fork 的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
## 轻量源修订号
派生状态的消费方会在加载完整事件日志之前比较一个低开销的不透明修订号。其表示由持久化后端拥有,并随 append 或会修改数据的 load 修复以事务方式改变;调用方仅比较修订号是否相等。
```ts type-equiv
/**
* Backend-owned token that identifies both one storage source and one revision
* of a persisted session log.
*/
type SessionPersistenceRevision = Branded<'SessionPersistenceRevision'>
```
```ts type-equiv
/** Lightweight immutable source identity returned without loading a full log. */
interface SessionPersistenceSnapshot {
/** Detached metadata for one materialized session. */
header: SessionHeader
/** Opaque source-qualified token that changes whenever this stored log changes. */
revision: SessionPersistenceRevision
}
```
## 后端
两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/load/inspect/list/listSnapshots并通过 `runPersistenceContract`,证明该 seam 确实与后端无关:
- **[dsh-session-persistence-jsonl](../../packages/session-persistence/session-persistence-jsonl)**——每个会话一份仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame也可配置为原始行支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。
- **[dsh-session-persistence-sqlite](../../packages/session-persistence/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行结构 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
sandbox.md: 9bc05fa06f22fdc9ac9e8aacd482c1e7c2f2edec
sandbox.zh.md: 9a52f126758fe0e7988715c7824e963bd6e6ea84

View File

@@ -1,5 +1,7 @@
# Process Sandbox
English | [中文](sandbox.zh.md)
The process-sandbox seam of [dsh-sandbox](../../packages/sandbox/sandbox) wraps a same-world subprocess argv in a file-effect policy without coupling consumers to a platform runner. [dsh-sandbox-local](../../packages/sandbox/sandbox-local) supplies the Linux bwrap/Landlock and macOS Seatbelt backends; [dsh-bash-sandbox](../../packages/bash/bash-sandbox) is the first consumer. Containers, microVMs, and remote execution are sibling implementations of whole capability seams, not providers of `ctx.sandbox`.
Source: [`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox/src/index.ts)

View File

@@ -0,0 +1,127 @@
# 进程沙箱
[English](sandbox.md) | 中文
[dsh-sandbox](../../packages/sandbox/sandbox) 的进程沙箱 seam 将与宿主共享文件系统和内核的子进程 argv 包装在文件效果策略中,而不将消费方耦合到特定平台运行器。[dsh-sandbox-local](../../packages/sandbox/sandbox-local) 提供 Linux bwrap/Landlock 与 macOS Seatbelt 后端;[dsh-bash-sandbox](../../packages/bash/bash-sandbox) 是第一个消费方。容器、microVM 和远程执行是完整能力 seam 的兄弟实现,而非 `ctx.sandbox` 的提供方。
源码:[`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox/src/index.ts)
## 模式与强制执行
`SandboxMode` 仅管控文件系统效果。`read-only` 拒绝所有写入(必需的 `/dev/null` 接收器除外);`workspace-write` 允许在工作区根目录及后端承诺的临时区域下写入;`danger-full-access` 绕过隔离。网络与进程可见性不在此处的定义范围内。
```ts type-equiv
/**
* File-effect policy for confined processes. `read-only` permits only required
* sinks such as `/dev/null`; `workspace-write` also permits the workspace and a
* backend-defined temp area; `danger-full-access` bypasses confinement. Network
* and process visibility are outside this vocabulary.
*/
type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
```
只有前两种模式可以发送给提供方。`danger-full-access` 的消费方直接 spawn 原始 argv不调用 `ctx.sandbox`。
```ts type-equiv
/** A confining (non-`danger-full-access`) mode — the modes a {@link SandboxPolicy} can carry. */
type ConfinedSandboxMode = Exclude<SandboxMode, 'danger-full-access'>
```
强制执行程度是一个报告事实。`full` 表示后端管控了该模式承诺的所有文件效果;`partial` 表示活跃后端或较旧的内核 ABI 仅管控其中一个子集,因此要求绝对保证的消费方必须拒绝或向上暴露这一区别。
```ts type-equiv
/**
* Enforcement completeness for this host. `partial` means an active backend or
* older kernel ABI cannot govern every promised file effect; callers requiring
* an absolute boundary must not treat it as `full`.
*/
type SandboxEnforcement = 'full' | 'partial'
```
## 逐调用策略
完整执行策略会按每次能力调用解析并携带。它包括 `danger-full-access`,因此消费方可以只解析一次策略,再决定是否绕过约束。普通工具调用从调用会话的不可变 cwd 派生 `workspaceRoot`;部署配置是没有 agent智能体时的回退值。root 会先按文件系统语义规范化,再做词法规范化,因此包含 `symlink/..` 的 cwd 会标识所生成进程实际运行的目录。
```ts type-equiv
/**
* The complete file-effect policy resolved for one capability call. The root
* is carried even under modes that do not consume it so callers can resolve
* policy once before choosing the enforcement path.
*/
interface SandboxExecutionPolicy {
/** The file-effect mode this execution runs under. */
mode: SandboxMode
/** Absolute root directory `workspace-write` may write under. */
workspaceRoot: string
}
```
`ctx.sandboxPolicy.resolve()` 接收活跃会话;对于已批准的重试,还接收显式模式。该服务拥有优先级与 root 回退规则,使 bash 和 fs 不必重复实现。
```ts type-equiv
/** Inputs that select the sandbox policy for one capability call. */
interface SandboxPolicyRequest {
/** Calling session; its immutable cwd becomes the workspace boundary. */
session?: Session
/** Explicit approved mode override, which outranks session policy. */
mode?: SandboxMode
}
```
只有受约束的执行会到达 `ctx.sandbox`;其提供方策略在保留同一 root 的同时收窄模式。这使并发会话、消费方与一次性提权重试可以向同一提供方请求不同边界,而无需改变提供方状态。
```ts type-equiv
/**
* What one confined execution is allowed to touch — carried PER CALL, not
* fixed on the provider: two consumers may confine under different policies
* at the same instant (bash under `read-only` while a confined child agent
* needs its state directory writable), and an approved escalated retry is a
* new call with a wider policy. Defaulting/resolution is an explicit step at
* the consumer boundary; the provider treats the policy as fully specified.
*/
interface SandboxPolicy extends SandboxExecutionPolicy {
/** The file-effect mode this execution runs under. */
mode: ConfinedSandboxMode
}
```
## 包装后的 argv 与分类方言
`ConfinedArgv` 是消费方实际 spawn 的内容。除了替换后的 argv它还携带后端的强制执行事实和两种正交的 stderr 方言。`denialSignatures` 用于识别沙箱正常工作时被隔离命令被阻止的情况。`runnerFailureSignatures` 用于识别沙箱运行器在执行命令之前拒绝或失败的情况;消费方应先检查后者,将其作为沙箱基础设施故障上报,而非普通任务失败。
```ts type-equiv
/**
* A {@link SandboxProvider.confine} result: the argv to spawn in place of
* the caller's own, plus the enforcement completeness the selected backend
* achieves for it.
*/
interface ConfinedArgv {
/** The wrapped argv (runner, profile, separator, then the caller's argv). */
argv: string[]
/** How completely the selected backend enforces the policy's file effects. */
enforcement: SandboxEnforcement
/**
* The selected backend's denial DIALECT: the case-insensitive stderr
* substrings a file effect denied by THIS backend produces (EROFS text
* under bwrap's read-only binds, EACCES under Landlock, EPERM under
* Seatbelt). A consumer that infers denials from a failed run's stderr
* matches against exactly these rather than a cross-backend union — the
* union claims denials a given backend never produces.
*/
denialSignatures: readonly string[]
/**
* Case-insensitive signatures for runner failure before command execution.
* Consumers check these before denial signatures: runner failure means the
* command never ran, while denial means confinement worked and blocked it.
*/
runnerFailureSignatures: readonly string[]
}
```
运维人员配置的本地运行器必须为自身的 pre-exec 拒绝方言提供至少一条 `runnerFailureSignatures` 条目;提供方会自动添加外层 shell 的 missing 和 unexecutable 形式。这使得可执行的自定义运行器拒绝其 profile 的情况能够与被包装命令以相同状态码退出的情况区分开来。
## 提供方与 fail-closed 错误
`ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。已选定的运行器也可能在执行时 fail-closed此时其失败签名承载相同的基础设施含义。对于受限策略静默的无隔离透传永远不合法。
提供方探测在多个候选后端之间仲裁,结果在提供方生命周期内缓存。只有一个候选后端的平台可以直接选定它;执行时拒绝仍保留安全属性。本地提供方将 bwrap 和 Seatbelt 报告为 full并保留 Landlock 启动器的 full/partial 内核裁定。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
scope.md: 73a697f2843293daffff85dabf4656346f7dcd04
scope.zh.md: f3c591da2befdcff69d89ad0667653392111fb8e

View File

@@ -1,5 +1,7 @@
# Scoped Registration
English | [中文](scope.zh.md)
The [scope package](../../packages/core/scope) supplies the identity, carrier, and scoped-layer vocabulary that makes one registration context mean both per-agent visibility and shared lifetime ownership. It is a library primitive rather than a Cordis service; the [agent-scope runtime-design Agent Note](../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#scope-routing-one-opaque-key-selects-one-layer) owns the lifecycle rationale, the [shared-storage Agent Note](../../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md) owns the registry-layer decision, and the package [README](../../packages/core/scope/README.md) owns the callable API and filtering semantics.
Sources: [`packages/core/scope/src/index.ts`](../../packages/core/scope/src/index.ts) and [`packages/core/scope/src/store.ts`](../../packages/core/scope/src/store.ts).

View File

@@ -0,0 +1,59 @@
# 作用域注册
[English](scope.md) | 中文
[scope 包package](../../packages/core/scope)提供 identity、carrier 与 scoped-layer 词汇,使同一个注册上下文同时代表逐 agent智能体可见性和共享生命周期所有权。它是库原语而不是 Cordis 服务;生命周期设计理由由 [agent-scope 运行时设计 Agent Noteagent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#scope-routing-one-opaque-key-selects-one-layer)规定,注册表层决策由[共享存储 Agent Note](../../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md)规定,可调用 API 与过滤语义则由包 [README](../../packages/core/scope/README.md)规定。
源码:[`packages/core/scope/src/index.ts`](../../packages/core/scope/src/index.ts) 与 [`packages/core/scope/src/store.ts`](../../packages/core/scope/src/store.ts)。
## 身份标识与分发载体
`ScopeKey` 是一个不透明的对象身份标识。已交付的 agent loop智能体循环使用活跃的 `Agent` 对象作为自身的 key但该原语从不检视该对象。
```ts type-equiv
/** An opaque, identity-compared scope key. */
type ScopeKey = object
```
`Scoped<T>` 是编译期品牌标记,标注在 `scopeTarget(base, key)` 返回的不透明路由接收器上。作用域过滤的事件声明要求以此载体作为 `this` 类型,而真正的事件主体仍作为显式参数传入。
```ts type-equiv
/**
* A routing-only event receiver built by {@link scopeTarget}. The type
* parameter records the subject type for dispatch checking; the carrier does
* not expose the subject's properties. Event payloads carry the real subject.
*/
type Scoped<T extends object> = object & { readonly [ScopedBrand]: T }
```
## 拥有所有权的注册上下文
`Scope` 将带标签的注册上下文与两个拆卸接口配对。`rawDispose` 保留有序复合 effect 所需的精确 Cordis disposer 身份;`dispose()` 是面向直接调用方和竞态调用方的公共停稳边界。
```ts type-equiv
/** A minted registration scope and its quiescent disposal boundaries. */
interface Scope {
/** Context through which scope-owned registrations are made. */
ctx: Context
/** Exact Cordis disposer, used when nesting this scope in an ordered composite effect. */
rawDispose: () => Promise<void> | void
/** Dispose every scope-owned registration; racing calls await the same completion. */
dispose(): Promise<void>
}
```
## 带作用域的注册表层
`ScopeLayer` 表示一个注册表在全局或确切作用域层级的完整贡献。具体 layer 可以聚合多个具名与匿名 table整个 layer 为空时,`ScopedLayers` 可以回收带作用域状态,而不会丢弃兄弟 table。
```ts type-equiv
/** One scope's aggregate contribution to a registry. */
interface ScopeLayer {
/** Whether every table in this layer is empty. */
isEmpty(): boolean
}
```
`ScopedLayers<L>` 拥有立即创建的全局 layer以及惰性创建的确切作用域 layer。读取不会创建 layer`peek(undefined)` 表示没有 overlay而 `merge()` 会物化按插入顺序排列的全局具名 entry随后是带作用域的 shadow。注册使用同一个上下文表示可见性与 Cordis effect 所有权,在可选通知前收集一个同步 undo返回 Cordis 的确切 disposer并且只在带作用域 layer 的完整 `ScopeLayer` 为空时回收它。
`NamedEntries<V>` 提供按插入顺序的查找与 live iteration重复错误由调用方所有。`AnonymousEntries<V>` 为每次 append 分配唯一标识,使相等的值仍相互独立。迭代在同一非空 table generation 内保持 live排空 table 会让现有 iterator 与后续插入脱离。两者都返回幂等的确切 entry undo共享的 `EntryValues` 实现接口不公开。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
session-query.md: c6dde8714a0875d46cf7b49cc181daab8f44afe0
session-query.zh.md: 44be8b7f89e1576e54c8dc3cb60619d6728c6895

View File

@@ -1,5 +1,7 @@
# Session Query
English | [中文](session-query.zh.md)
Query vocabulary over the live-preferred logical session corpus. The [interface package](../../packages/session-query/session-query) owns exact reads, source precedence, relationship tracing, semantic extraction, and provider-independent filters, while the [SQLite package](../../packages/session-query/session-query-sqlite) owns the concrete full-text index lifecycle.
Source: [`packages/session-query/session-query/src/types.ts`](../../packages/session-query/session-query/src/types.ts)

View File

@@ -0,0 +1,304 @@
# 会话查询
[English](session-query.md) | 中文
本文定义面向优先使用 live 数据的逻辑会话语料库的查询词汇。[接口包package](../../packages/session-query/session-query)负责精确读取、来源优先级、关系追踪、语义提取,以及与提供方无关的过滤器;[SQLite 包](../../packages/session-query/session-query-sqlite)负责具体全文索引的生命周期。
源码:[`packages/session-query/session-query/src/types.ts`](../../packages/session-query/session-query/src/types.ts)
## 逻辑记录
`SessionRecord` 由跨语料库列表返回。它独立于克隆后的实时优先 header 暴露源可用性。`SessionEventRecord` 是轻量的原始日志投影;分类使用与 model-history 推导相同的 `foldSurface()` 状态转换。
```ts type-equiv
/** Whether an event is current model context, replaced context, or raw-log-only. */
type SessionEventSurface = 'current' | 'shadowed' | 'log-only'
```
```ts type-equiv
/** Lightweight identity and source availability for one logical session. */
interface SessionRecord {
/** Cloned session header selected from the live-preferred corpus. */
header: SessionHeader
/** Whether the id currently exists in `ctx.sessions`. */
live: boolean
/** Whether the active persistence backend currently materializes the id. */
persisted: boolean
}
```
`SessionLogSnapshot` 是供恢复预检使用的完整原始日志:它脱离运行时,并经过回放验证。`SessionSurfaceSnapshot` 表示一次精确读取的 surface 观测结果,而不是持续保留的订阅。
```ts type-equiv
/** One validated detached observation of a logical session's complete raw log. */
interface SessionLogSnapshot {
/** Cloned session header selected from the same observation as `events`. */
session: SessionHeader
/** Cloned contiguous raw events after persistence repair and replay validation. */
events: SessionEvent[]
}
```
```ts type-equiv
/** One atomic live-preferred observation of a session's current model surface. */
interface SessionSurfaceSnapshot {
/** Cloned session header selected from the same corpus observation as `events`. */
session: SessionHeader
/** Highest raw-log seq included in the observation, or `null` for an empty log. */
capturedThroughSeq: number | null
/** Cloned current surface events in model-history order. */
events: SurfaceEvent[]
}
```
```ts type-equiv
/** Lightweight metadata for one event within a logical session. */
interface SessionEventRecord {
/** Session that owns the event. */
sessionId: SessionId
/** Monotonic event seq within the session. */
seq: number
/** Discriminant of the session event. */
type: SessionEventType
/** Event timestamp in Unix epoch milliseconds. */
time: number
/** Event placement in the folded session surface. */
surface: SessionEventSurface
}
```
## 与提供方无关的过滤器和文档
会话和事件过滤器数组内的各项按逻辑与AND组合单个列表子句中的各值按逻辑或OR组合。范围包含两端。事件的 `text` 子句会对提取出的语义文本执行正则表达式扫描搜索文本按字面量处理Unicode 字符不区分大小写,空白字符可灵活匹配;该过程与全文搜索提供方无关。
```ts type-equiv
/**
* One logical-session predicate. A filter array is ANDed; `values` within a
* clause are ORed.
*/
type SessionResultFilter =
| { kind: 'id'; values: readonly SessionId[] }
| { kind: 'cwd'; values: readonly (string | null)[] }
| ({ kind: 'created-at' } & SessionResultRange)
| { kind: 'parent'; values: readonly (SessionId | null)[] }
| { kind: 'availability'; values: readonly SessionAvailability[] }
```
```ts type-equiv
/**
* One event predicate. A filter array is ANDed; list-valued clauses are ORed.
* Text is a literal, case-insensitive, whitespace-flexible semantic-text scan.
*/
type SessionEventResultFilter =
| ({ kind: 'seq' } & SessionResultRange)
| ({ kind: 'time' } & SessionResultRange)
| { kind: 'type'; values: readonly SessionEventType[] }
| { kind: 'surface'; values: readonly SessionEventSurface[] }
| { kind: 'text'; text: string }
```
```ts type-equiv
/** Searchable semantic document derived from one session event. */
interface SessionEventSearchDocument extends SessionEventRecord {
/** First-party semantic text used by scan filters and full-text indexes. */
text: string
}
```
`ctx.sessionQuery.filterSessions(filters)` 会对完整的逻辑会话语料库应用 `SessionResultFilter``ctx.sessionQuery.filterEvents(sessionId, filters)` 按 seq 升序返回匹配的文档。消息、推理reasoning、工具调用和工具结果、被阻止的提示词、待办事项以及失败和状态详情会纳入语义文本结构事件和流分片则不会。
## 全文搜索结果页
整合后的 `ctx.sessionQuery` seam 提供两个全文搜索范围。`searchSessions()` 按匹配度最强的事件对语料库分组;`searchEvents()` 搜索单个会话。请求将不透明游标与规范化后的查询、元数据过滤器和结果数量上限绑定。提供方的元数据过滤器有意不包含事件文本扫描。
```ts type-equiv
/** Provider-owned opaque continuation token returned by session search. */
type SessionSearchCursor = Branded<'SessionSearchCursor'>
```
```ts type-equiv
/** Cross-session full-text search request. */
interface SessionSearchRequest {
/** Full-text query interpreted as data, never executable FTS syntax. */
query: string
/** Logical-session predicates applied before event ranking. */
sessionFilters?: readonly SessionResultFilter[]
/** Event predicates applied before event ranking. */
eventFilters?: readonly SessionEventMetadataFilter[]
/** Maximum sessions in this page. */
limit?: number
/** Opaque cursor returned for the identical normalized request. */
cursor?: SessionSearchCursor
}
```
```ts type-equiv
/** Within-session full-text search request. */
interface SessionEventSearchRequest {
/** Session whose live-preferred logical log is searched. */
sessionId: SessionId
/** Full-text query interpreted as data, never executable FTS syntax. */
query: string
/** Event predicates applied before ranking. */
filters?: readonly SessionEventMetadataFilter[]
/** Maximum events in this page. */
limit?: number
/** Opaque cursor returned for the identical normalized request. */
cursor?: SessionSearchCursor
}
```
```ts type-equiv
/** One cursor-paginated result page. */
interface SessionSearchPage<T> {
/** Results for this page in contract-defined order. */
items: readonly T[]
/** Opaque continuation cursor, absent on the final page. */
nextCursor?: SessionSearchCursor
}
```
```ts type-equiv
/** One event full-text search hit with a bounded plain-text excerpt. */
interface SessionEventSearchHit extends SessionEventRecord {
/** Plain text excerpt selected around the match. */
snippet: string
}
```
```ts type-equiv
/** One grouped cross-session hit, ranked by its strongest matching event. */
interface SessionSearchHit extends SessionRecord {
/** Strongest matching event for this session. */
bestMatch: SessionEventSearchHit
}
```
## 会话谱系
`SessionLineageTrace` 按由近及远的顺序携带已知 parent并携带一片由直接 descendant 递归嵌套而成的森林。完整性判别字段使已知 root 与缺失 parent 互斥。
```ts type-equiv
/** Recursive descendant node in a session-lineage trace. */
interface SessionLineageNode {
/** Detached logical-corpus record for this descendant. */
session: SessionRecord
/** Direct children, each carrying its own recursive descendants. */
descendants: SessionLineageNode[]
}
```
```ts type-equiv
/** Known ancestry and descendants for one logical session. */
type SessionLineageTrace = {
/** Detached record for the session that was traced. */
target: SessionRecord
/** Known parents from the immediate parent outward. */
ancestors: SessionRecord[]
/** Complete known descendant trees rooted at the target's direct children. */
descendants: SessionLineageNode[]
} & (
| {
/** The complete parent chain is present in the logical corpus. */
complete: true
/** Detached record at the top of the complete lineage. */
root: SessionRecord
}
| {
/** The parent chain leaves the visible logical corpus. */
complete: false
/** First parent id that is not present in the logical corpus. */
unresolvedParentId: SessionId
}
)
```
## 有界事件读取
请求指定一个原始 seq 及可选的邻近数量。结果携带 `SessionHeader` 而非可用性标志,使已知的实时目标可以独立于持久化健康状态。
```ts type-equiv
/** Request for one event plus raw neighboring log context. */
interface SessionEventReadRequest {
/** Session that owns the target event. */
sessionId: SessionId
/** Target event seq. */
seq: number
/** Number of preceding raw events to include. */
before?: number
/** Number of following raw events to include. */
after?: number
}
```
```ts type-equiv
/** Full target event and a bounded raw-log window. */
interface SessionEventWindow {
/** Cloned header for the live-preferred source read. */
session: SessionHeader
/** Full cloned target event. */
target: SessionEvent
/** Full cloned events from `startSeq` through `endSeq`. */
events: SessionEvent[]
/** First seq included in `events`. */
startSeq: number
/** Last seq included in `events`. */
endSeq: number
}
```
## 事件关系
事件追踪会区分位置性的 surface 替换与已记录 provenance。除 `replacementChain` 外,每个 seq 列表都包含直接链接;该链从目标沿直接 replacer 追踪到最终的位置替换。
```ts type-equiv
/** Request for direct surface and provenance relationships around one event. */
interface SessionEventTraceRequest {
/** Session that owns the target event. */
sessionId: SessionId
/** Target event seq. */
seq: number
}
```
```ts type-equiv
/** Direct surface and provenance relationships for one event. */
interface SessionEventTrace {
/** Lightweight target record. */
target: SessionEventRecord
/** Immediate positional replacement event, when the target was shadowed. */
replacedBy?: number
/** Positional replacers from the immediate replacement to the final replacement. */
replacementChain: number[]
/** Surface nodes directly removed when the target itself performed a replacement. */
replacedEventSeqs: number[]
/** Direct logged provenance sources in their recorded order. */
sourceEventSeqs: number[]
/** Later events that directly name the target as a provenance source, in log order. */
derivedEventSeqs: number[]
}
```
## 错误
封闭的 code 联合类型区分请求校验、目标缺失、surface 日志格式错误、可选后端故障与矛盾的源元数据。
```ts type-equiv
/** Stable machine-routable failure taxonomy for session reads, traces, and search. */
type SessionQueryErrorCode =
| 'SESSION_QUERY_ABORTED'
| 'SESSION_QUERY_EVENT_NOT_FOUND'
| 'SESSION_QUERY_INDEX_FAILED'
| 'SESSION_QUERY_INVALID_CONFIG'
| 'SESSION_QUERY_INVALID_CURSOR'
| 'SESSION_QUERY_INVALID_FILTER'
| 'SESSION_QUERY_INVALID_LIMIT'
| 'SESSION_QUERY_INVALID_QUERY'
| 'SESSION_QUERY_INVALID_LINEAGE'
| 'SESSION_QUERY_INVALID_SURFACE'
| 'SESSION_QUERY_INVALID_WINDOW'
| 'SESSION_QUERY_PERSISTENCE_FAILED'
| 'SESSION_QUERY_SESSION_NOT_FOUND'
| 'SESSION_QUERY_STALE_CURSOR'
| 'SESSION_QUERY_SOURCE_CONFLICT'
```

View File

@@ -36,7 +36,7 @@ interface SessionReferenceCandidate {
## Prepared messages
Preparation preserves readable current-message content and returns at most one aggregated context. The host binds `contexts` to that exact `send()` or `steer()` call.
Preparation preserves readable current-message content and returns at most one aggregated context. The host binds `contexts` to that exact `followup()` or `steer()` call.
```ts type-equiv
/** Message payload and the zero-or-one durable snapshot contexts bound to it. */

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
session.md: 7a9ae919fecef7db19f3ca67feb128bcba1db4f0
session.zh.md: 9e1e9a4f39ed6919416ff5cebf345b43bbc450ea

View File

@@ -1,5 +1,7 @@
# Sessions
English | [中文](session.zh.md)
The in-memory, event-sourced model of [dsh-session](../../packages/core/session). A `Session` is an **append-only log** of typed `SessionEvent`s — the single source of truth for an agent's whole interaction history. The LLM message history is *derived* from the log, never stored separately; replay is re-derivation from the same events. How the log is made **durable** (the persistence seam, backends, crash recovery) is the sibling concern on [persistence.md](persistence.md).
Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
@@ -9,7 +11,13 @@ Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/t
The append-only event types. Merge-extensible: a plugin declares extra event types via declaration merging — e.g. the [compaction seam](compaction.md) adds `compact/start` / `compact/summary` / `compact/end`, and `@deepseek-ai/dsh-hook-protocol` adds log-only `hook/invoked` / `hook/result` provenance for a hook bridge. Like `compact/*`, these are NOT `SurfaceEventType`s (no `surfaceOp`). The generated [persistence log event catalog](../persistence-catalog.md) enumerates every member — core and merged — with its payload, surface badge, and declaration site.
```ts type-equiv
/** Shared payload for ordinary and steering prompt messages. */
/**
* Shared payload for user, injected-context, and steering prompt messages. A
* direct human prompt, a synthetic `agent.inject()` context, and mid-turn
* steering all project into the model transcript as verbatim user-role content;
* they are told apart by `source` (a non-`user` kind marks injected context),
* not by event type. `meta` carries durable model-hidden producer state.
*/
interface PromptMessageData {
/** Exact model-facing blocks, including any baked prompt-prefix contexts. */
content: ContentBlock[]
@@ -17,6 +25,15 @@ interface PromptMessageData {
source: MessageSource
/** Present only when prompt-prefix contexts were baked into `content`. */
envelope?: PromptMessageEnvelope
/**
* Opaque durable JSON state retained on the event but hidden from the model
* projection. It is the intended channel for a future framing directive (a
* producer declares the frame, a dedicated renderer applies it — see the
* deferred note in
* ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md),
* so the surface keeps projecting `content` verbatim rather than wrapping it.
*/
meta?: JsonValue
}
```
@@ -46,29 +63,21 @@ interface SessionEventMap {
'step/start': { turn: number; step: number }
/** Closes step `step` of turn `turn`. */
'step/end': { turn: number; step: number }
/** A user-visible prompt (the queued message claimed for this turn). */
/**
* A user-role message on the model-visible surface: a direct human prompt
* (the queued message claimed for this turn), a synthetic `agent.inject()`
* context (file-change notices, subdir AGENTS.md, skill content, cron
* notifications, …), or an admitted goal continuation round. All three
* project their `content` verbatim; `source` (with a non-`user` kind marking
* injected context) is the only channel that tells them apart. An idle
* injection wraps this event in a one-shot turn so the log stays turn-enclosed.
*/
'user/message': PromptMessageData
/**
* Durable record of a prompt veto and its reason. It is log-only: the blocked
* prompt never enters the model-visible surface, and its turn runs zero steps.
*/
'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string }
/**
* In-session context injection (file-change notices, subdir AGENTS.md,
* skill content, cron notifications, …). Rendered into the derived history
* as a synthetic user-role message carrying `content` verbatim — NOT a
* user prompt. `meta` is durable JSON state omitted from the model
* projection; it is also the intended channel for any future framing
* directive (a producer declares the frame, a dedicated renderer applies it —
* see the deferred note in
* ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md),
* so the surface keeps projecting `content` verbatim rather than wrapping it.
*/
'context/message': {
content: ContentBlock[]
source: MessageSource
meta?: JsonValue
}
/** Raw stream chunk — token-level replay fidelity. */
'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
/**
@@ -199,7 +208,7 @@ A proper discriminated union over `type` (not independent `type`/`data` unions),
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`, `context/message`, `steering/message`).
* `assistant/message`, `tool/result`, `steering/message`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
@@ -233,7 +242,7 @@ For `assistant/message`, a present `sourceEventSeqs: []` is a complete known-emp
## Surface types
The five message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`) carry surface metadata declaring how they join the ordered derived surface. See the [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md).
The four message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `steering/message`) carry surface metadata declaring how they join the ordered derived surface. See the [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md).
### `SurfaceEventType` — the message-producing subset of event types
@@ -247,7 +256,6 @@ type SurfaceEventType =
| 'user/message'
| 'assistant/message'
| 'tool/result'
| 'context/message'
| 'steering/message'
```
@@ -258,7 +266,7 @@ type SurfaceEventType =
* How a session event entered the ordered surface. Only valid on
* {@link SurfaceEventType} events.
*
* - `'append'`: added to the tail — normal path for user/assistant/tool/context
* - `'append'`: added to the tail — normal path for user/assistant/tool/steering
* messages.
* - `{ op: 'replace', start, end }`: replaces surface nodes from `start`
* (inclusive) through `end` (inclusive) with this node. Both must exist as
@@ -465,7 +473,7 @@ declare class Session {
- `user/message` → a user message carrying exact `content`; an optional envelope remains log-only display metadata.
- `assistant/message` → an assistant message with the event's provider/model provenance and optional adapter-private replay state. Raw `assistant/chunk` events are replay/UI data and are **skipped** in derivation (the assembled message is authoritative). An **empty-content** `assistant/message` is also skipped — a max-tokens step cut off with no content still records an `assistant/message` to host its usage/provenance, but a content-less assistant turn must not enter the provider transcript.
- `tool/result` → a user message carrying a `tool-result` block.
- `context/message` → a user-role message carrying its `content` verbatim at its chronological position. Optional JSON `meta` remains in the event log and is never rendered.
- `user/message` (injected context, i.e. non-`user` source) → a user-role message carrying its `content` verbatim at its chronological position. Optional JSON `meta` remains in the event log and is never rendered.
- `steering/message` → a user-role message carrying exact `content` at its chronological position; an optional envelope remains log-only display metadata.
Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and does not project into a message. Token accounting reads per-step `assistant/chunk { type: 'usage' }` records and treats `assistant/message.usage` as the committed-step fallback when no usage chunk exists; failed model-request attempts have no assistant message, so their usage chunk is the durable accounting record. An operational error's step number is on `turn/end.reason` for `kind: 'error'`, with normalized `LlmFailure` facts for a final model-request failure and message/code for other live errors. Because this unreleased format intentionally has no compatibility promise, seed/load validation rejects request headers without provider+model and assistant messages without provider/model provenance instead of guessing a route for historical data.
@@ -489,11 +497,12 @@ interface TurnTriggerMap {
message: { kind: 'message'; source: MessageSource }
/**
* An out-of-band context injection (`agent.inject()`) made while the agent
* was idle. The loop wraps the injected `context/message` in a one-shot turn
* (`turn/start` → `context/message` → `turn/end`) so every event in the log
* stays turn-enclosed — the durability/replay boundary is the turn, and a
* bare event between turns would otherwise be indistinguishable from a crash
* tail on reload.
* was idle. The loop wraps the injected `user/message` (a non-`user` source,
* plugin by default) in a one-shot turn (`turn/start` → `user/message` →
* `turn/end`) so every event in the log stays turn-enclosed — the
* durability/replay boundary is the turn, and a bare event between turns would
* otherwise be indistinguishable from a crash tail on reload. The trigger's
* `source` mirrors that message's producer.
*/
injection: { kind: 'injection'; source: MessageSource }
}
@@ -542,13 +551,13 @@ interface TurnEndReasonMap {
## The turn-enclosure invariant
Every session event lives **inside** a turn (between a `turn/start` and its `turn/end`). The loop appends queued `user/message` events *after* `turn/start`, an idle `agent.inject()` wraps its `context/message` in a one-shot `injection` turn, and `appendOutOfBand()` similarly wraps an eligible log-only event when no turn is open. This makes the turn the single durability/replay boundary: a backend can treat anything after the last `turn/end` as an interrupted-crash tail without risking the loss of legitimately-recorded between-turn context. The optional `dsh-session/invariant` companion enforces it in dev through `ctx.invariants` (a message event outside an open turn throws). See [the turn-enclosure invariant Agent Note](../../.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md).
Every session event lives **inside** a turn (between a `turn/start` and its `turn/end`). The loop appends queued `user/message` events *after* `turn/start`, an idle `agent.inject()` wraps its `user/message` in a one-shot `injection` turn, and `appendOutOfBand()` similarly wraps an eligible log-only event when no turn is open. This makes the turn the single durability/replay boundary: a backend can treat anything after the last `turn/end` as an interrupted-crash tail without risking the loss of legitimately-recorded between-turn context. The optional `dsh-session/invariant` companion enforces it in dev through `ctx.invariants` (a message event outside an open turn throws). See [the turn-enclosure invariant Agent Note](../../.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md).
## Plugin-contributed log-only events
A plugin may declaration-merge extra `SessionEventMap` types. These are **log-only**: NOT `SurfaceEventType`s (they carry no `surfaceOp` and contribute nothing to derived history), but, like every event, they must sit inside an open turn. The full per-event enumeration — core and plugin-contributed alike, with payloads and provenance — is the generated [persistence log event catalog](../persistence-catalog.md); the compaction seam's `compact/*` semantics are discussed on [compaction.md](compaction.md).
The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. The mid-turn hook points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record — its injected `context/message` is the durable evidence — because it has no open turn to enclose one (see [the hook-bridges Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md)).
The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. The mid-turn hook points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record — its injected `user/message` is the durable evidence — because it has no open turn to enclose one (see [the hook-bridges Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md)).
## Durability contract

View File

@@ -0,0 +1,570 @@
# 会话
[English](session.md) | 中文
[dsh-session](../../packages/core/session) 的内存事件溯源模型。`Session` 是一份由类型化 `SessionEvent` 组成的**仅追加日志**,是 agent智能体完整交互历史的唯一真源。LLM大语言模型消息历史从日志*派生*而来,从不单独存储;回放即从同一组事件重新派生。日志如何实现**持久化**(持久化 seam、后端、崩溃恢复是兄弟文档 [persistence.md](persistence.md) 的关注点。
源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
## `SessionEventMap`:事件词汇
仅追加的事件类型。可通过声明合并扩展:插件通过 declaration merging 声明额外的事件类型。例如[压缩compaction seam](compaction.md) 添加了 `compact/start` / `compact/summary` / `compact/end``@deepseek-ai/dsh-hook-protocol` 添加了仅记录日志的 `hook/invoked` / `hook/result` 溯源事件,用于钩子桥接。与 `compact/*` 一样,这些都不是 `SurfaceEventType`(没有 `surfaceOp`)。生成的[持久化日志事件目录](../persistence-catalog.md)列举了所有成员(核心与合并扩展的),包含其 payload、surface 标记与声明位置。
```ts type-equiv
/**
* Shared payload for user, injected-context, and steering prompt messages. A
* direct human prompt, a synthetic `agent.inject()` context, and mid-turn
* steering all project into the model transcript as verbatim user-role content;
* they are told apart by `source` (a non-`user` kind marks injected context),
* not by event type. `meta` carries durable model-hidden producer state.
*/
interface PromptMessageData {
/** Exact model-facing blocks, including any baked prompt-prefix contexts. */
content: ContentBlock[]
/** Producer provenance for the direct prompt. */
source: MessageSource
/** Present only when prompt-prefix contexts were baked into `content`. */
envelope?: PromptMessageEnvelope
/**
* Opaque durable JSON state retained on the event but hidden from the model
* projection. It is the intended channel for a future framing directive (a
* producer declares the frame, a dedicated renderer applies it — see the
* deferred note in
* ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md),
* so the surface keeps projecting `content` verbatim rather than wrapping it.
*/
meta?: JsonValue
}
```
```ts type-equiv
/**
* The merge-extensible, append-only source of truth for an agent interaction.
* Message history is derived from this log. Every event is lossless JSON and
* sequence numbers stay contiguous, including raw chunks, so persistence can
* store the canonical log verbatim.
*/
interface SessionEventMap {
/**
* Opens turn `turn`. `trigger` records what started it — one claimed queued
* message or an idle-time injection. The turn is the durability/replay
* boundary: every event sits between a `turn/start` and its matching
* `turn/end` (the turn-enclosure invariant).
*/
'turn/start': { turn: number; trigger: TurnTrigger }
/**
* Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop
* awaits `session/flush` after an ordinary turn ends before claiming the next
* queued item. Success commits the turn; rejection is reported live and does
* not prevent later work.
*/
'turn/end': { turn: number; reason: TurnEndReason }
/** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */
'step/start': { turn: number; step: number }
/** Closes step `step` of turn `turn`. */
'step/end': { turn: number; step: number }
/**
* A user-role message on the model-visible surface: a direct human prompt
* (the queued message claimed for this turn), a synthetic `agent.inject()`
* context (file-change notices, subdir AGENTS.md, skill content, cron
* notifications, …), or an admitted goal continuation round. All three
* project their `content` verbatim; `source` (with a non-`user` kind marking
* injected context) is the only channel that tells them apart. An idle
* injection wraps this event in a one-shot turn so the log stays turn-enclosed.
*/
'user/message': PromptMessageData
/**
* Durable record of a prompt veto and its reason. It is log-only: the blocked
* prompt never enters the model-visible surface, and its turn runs zero steps.
*/
'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string }
/** Raw stream chunk — token-level replay fidelity. */
'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
/**
* Assembled assistant message for one step (derived history uses this).
* Carries the step's `usage` when the adapter reported token accounting, so
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
*/
'assistant/message': { turn: number; step: number; content: ContentBlock[]; provenance: AssistantProvenance; usage?: TokenUsage }
/**
* The model requested one tool invocation: `name` with the raw `arguments`
* JSON string exactly as the model produced it (unparsed). `callId` pairs the
* call with its `tool/result`.
*/
'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string }
/**
* A completed tool call's model-facing result, optional internal failure
* identity, and optional tool-private `meta` presentation payload. `meta` is
* opaque to the core (the producing tool owns its shape and reads it back in
* `presentResult`) but MUST be JSON-serializable: `Session.append`
* runtime-validates all event data with `isJsonValue`, so a non-serializable
* `meta` is rejected at the source, and the durable log reproduces the
* identical card on replay. Absent
* unless the tool attaches one (e.g. `dsh-tool-fs` carries its result-time
* contextual diff here).
*/
'tool/result': {
turn: number
step: number
callId: CallId
content: ContentBlock[]
isError: boolean
error?: { name: string; code: string }
meta?: JsonValue
}
/** Steering content injected between steps of a running turn. */
'steering/message': PromptMessageData & { turn: number }
/** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */
'todo/write': { todos: TodoItem[] }
/**
* Full header for the next request, appended inside its step before dispatch.
* It is log-only; the latest snapshot reconstructs the request header.
*/
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
}
```
`PromptMessageData.content` 始终是确切的模型可见内容。当附加上下文声明 `prompt-prefix` 放置方式时AgentLoop 会依次把它的块、一个 `## My request:` 分隔符以及最终生效的直接提示词拼接进该数组。可选且对模型隐藏的 `envelope` 会保留 `displayContent`,以及按顺序排列的前缀上下文来源/元数据描述信息,使 transcript文本记录、标题与重新引用消费方无需改变可重建历史就能呈现人类提示词。`displayPromptContent()` 负责该选择,并为普通事件和较早的事件回退到 `content`。
### `OutOfBandSessionEventMap`:受限的带外追加显式准入
仅属于 `SessionEventMap` 并不表示事件可以脱离 agent loop智能体循环的常规生命周期追加。事件所有方必须通过声明合并将同一键加入这个空标记映射`ctx.sessions.appendOutOfBand()` 才会接受该事件;派生类型还会排除所有 surface 事件。被接受的更新会并入已打开的轮次;如果没有打开的轮次,系统则为它创建一个边界配平且已刷新完成的零步骤轮次。
```ts type-equiv
/**
* Marker map for plugin-owned log-only events accepted by
* `SessionStore.appendOutOfBand()`. A plugin extends this map with the same key
* it adds to {@link SessionEventMap}; surface and lifecycle events stay
* ineligible unless their owner explicitly opts them into this narrow seam.
*/
interface OutOfBandSessionEventMap {}
```
### `TodoItem`:一条待办项
这是 `todo/write` 事件全量列表快照中的单元。它有意保持精简:一行 `content` 加一个三态 `status`(没有 id、优先级或 `activeForm`);列表在每次写入时整体替换,因此条目无需稳定标识,而这三个状态值恰好对应 ACP 的 `PlanEntryStatus`,所以 UI 桥接层可以将待办列表一一映射为 ACP `plan`(并合成 ACP 额外要求的优先级)。见 [todo_write Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-06-29-todo-write-tool.md)。
```ts type-equiv
/**
* One entry in an agent's todo list — the unit of the `todo/write`
* {@link SessionEventMap} event's whole-list snapshot.
*
* Deliberately minimal: a human-readable `content` line and a three-state
* `status`. No id, priority, or `activeForm` — the list is replaced wholesale
* on every write (last-write-wins), so entries need no stable identity, and the
* status triple is exactly the ACP `PlanEntryStatus`, so a UI bridge can map a
* todo list onto an ACP `plan` 1:1 (synthesizing the priority ACP additionally
* requires).
*/
interface TodoItem {
/** What this task is — a short imperative line shown in the UI. */
content: string
/** Lifecycle state. `in_progress` marks the single task being worked now. */
status: 'pending' | 'in_progress' | 'completed'
}
```
<a id="the-request-header-event-requestheader"></a>
### 请求头事件:`request/header`
请求信封(即 `EpochHeader`:调用配置 + 渲染后的系统提示词 + 已组装的工具 schema + 会话前缀)会作为会话状态写入日志,因此每个对话请求都是日志的纯函数(见可重建性 Agent Note。带有 reason `'initial'` 或 `'resume'` 的完整 `request/header` 快照记录每个 agent loop 实例的边界;之后请求发生变化时,系统会以 reason `'change'` 记录另一份完整快照。`foldRequestHeader(events)` 通过选择最新快照重建请求头。该事件不是 `SurfaceEventType`,不产生 LLM 消息。
```ts type-equiv
/**
* Logged request state outside derived history: call config, system prompt,
* tools, and prefix. The latest full `request/header` snapshot reconstructs it;
* canonical empty optional fields are absent.
*/
interface EpochHeader {
/** The conversation's call configuration (provider, model, and sampling scalars). */
config: LlmCallConfig
/** Rendered system prompt text; absent for a system-less request. */
system?: string
/** Assembled tool schemas; absent for a tool-less request. */
tools?: ToolSchema[]
/**
* The session prefix: request-only messages sent BEFORE the entire derived
* history (the `agent/session-prefix` waterfall's product, composed once
* per loop instance and reused for every request it sends). Not session
* history — `deriveMessages()` never returns it — so the header is its
* only durable record; absent when the instance composed none.
*/
messagePrefix?: Message[]
}
```
规范形式:空系统提示词、空工具列表和空会话前缀都表示为字段缺失,与请求构建方式一致。`messagePrefix` 是 `agent/session-prefix` waterfall瀑布式事件产物的持久记录请求 = `messagePrefix + derived history`);每个 agent loop 实例只组合一次,并包含在该实例记录的每份完整快照中。包含已移除的 `request/header-delta` 事件或完整快照原因为 `fallback` 的旧版 v0 日志,会在 seed、append 和持久化加载边界被拒绝,而不会以不完整方式回放。
## `SessionEvent<T>`:一条日志条目
基于 `type` 的真正可辨识联合(而非独立的 `type`/`data` 联合),因此 `switch (event.type)` 能直接收窄 `event.data`,无需类型断言。`seq` 是日志中的单调递增位置(`seq = log.length``time` 为 epoch 毫秒。
```ts type-equiv
/**
* One immutable entry in the session log.
*
* A proper discriminated union over `type` (not independent `type`/`data`
* unions), so `switch (event.type)` narrows `event.data` without casts.
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`, `steering/message`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
*/
type SessionEvent<T extends SessionEventType = SessionEventType> = {
[K in SessionEventType]: {
type: K
/** Monotonic sequence number within the session. */
seq: number
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of events that are provenance sources of this event
* (e.g. the `assistant/chunk` seqs that built an `assistant/message`,
* or the surface nodes shadowed by a compaction replace node). An
* `assistant/message` may carry a present empty array for a known empty
* provider stream; omission means unrecorded provenance.
*/
sourceEventSeqs?: number[]
/** How this event entered the surface; absent for non-surface events. */
surfaceOp?: SurfaceOp
} : object)
}[T]
```
`SessionEventType = keyof SessionEventMap`。由于 `SessionEventMap` 可通过合并扩展,对 `SessionEvent` 的 switch 语句禁止使用 `assertNever`:插件添加的变体是合法的未知值;处理已知 case 后在 `default` 中放行。
对于 `assistant/message`,存在的 `sourceEventSeqs: []` 表示提供方流已知且完整地为空字段缺失则表示旧格式或其他未记录溯源信息的情况。agent loop 会为每次成功的模型调用写入该字段;其他 surface 事件只要包含该字段,其列表就必须非空。
## Surface 类型
四种产生消息的类型(`SurfaceEventType``user/message`、`assistant/message`、`tool/result`、`steering/message`)携带 surface 元数据,用来声明它们如何加入有序的派生 surface。见 [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md)。
### `SurfaceEventType`:事件类型中产生消息的子集
```ts type-equiv
/**
* The subset of {@link SessionEventType} values whose events produce LLM
* messages and are eligible to appear on the ordered surface. Only these
* event types may carry {@link SurfaceOp} and {@link SessionEvent.sourceEventSeqs}.
*/
type SurfaceEventType =
| 'user/message'
| 'assistant/message'
| 'tool/result'
| 'steering/message'
```
### `SurfaceOp`:事件如何进入 surface
```ts type-equiv
/**
* How a session event entered the ordered surface. Only valid on
* {@link SurfaceEventType} events.
*
* - `'append'`: added to the tail — normal path for user/assistant/tool/steering
* messages.
* - `{ op: 'replace', start, end }`: replaces surface nodes from `start`
* (inclusive) through `end` (inclusive) with this node. Both must exist as
* surface nodes in the current surface. `start === end` replaces a single
* node. The node's {@link SessionEvent.sourceEventSeqs} must include every
* shadowed surface node. Used by compaction and possible other manipulations.
*/
type SurfaceOp =
| 'append'
| { op: 'replace'; start: number; end: number }
```
`'append'` 是常规的尾部追加路径。`replace` 会遮蔽从 `start` 到 `end`(含两端)的 surface 条目(两者都必须是有效的 surface seq`start === end` 时仅替换单个条目),并在原位置插入新事件。
### `SurfaceIntent``session.append()` 的参数
```ts type-equiv
/**
* Surface placement and provenance for {@link Session.append}. Required on
* message-producing events and forbidden on log-only events.
*/
interface SurfaceIntent {
surfaceOp: SurfaceOp
/**
* Complete known provenance source set. `assistant/message` may use a
* present empty array for a known empty provider stream; omission means its
* provenance was not recorded. Other surface events require a non-empty set
* when this field is present.
*/
sourceEventSeqs?: number[]
}
```
对 `SurfaceEventType` 事件必填:每个产生消息的事件都必须声明它如何加入 surface派生历史的唯一来源。非 surface 类型在编译期拒绝此参数。
此处适用相同的溯源区分:只有 `assistant/message` 可以携带存在但为空的 `sourceEventSeqs`;省略该字段并不表示其源流为空。
### `SessionSurface`:实时只读 surface 投影
`Session.surface` 返回会话稳定的 `SessionSurface` 视图。同一个增量管理器在提交前校验追加候选事件,并根据已提交事件推进该投影;调用方可以观察成员关系和替换代次,但不能调用校验。
```ts type-equiv
/** Readonly live projection of the message-producing session events. */
interface SessionSurface {
/** Current surface event sequences in model-visible order. */
readonly nodes: readonly number[]
/** Monotonic count of committed positional replacements. */
readonly replaceGeneration: number
}
```
### `SurfaceFoldReplacement` 与 `SurfaceFoldResult`:完整的 surface 回放
`foldSurface(events)` 返回一份独立的当前事件 seq 列表,以及每个声明的替换范围实际遮蔽的 seq。实时管理器复用同一套状态转换但不保留替换历史。每提交一次替换其 `replaceGeneration` 就递增一次,使增量消费方能够区分纯尾部增长与重写。
```ts type-equiv
/** One replacement operation observed while folding a session surface. */
interface SurfaceFoldReplacement {
/** Seq of the event that replaced the prior surface range. */
seq: number
/** Declared inclusive start seq of the replaced surface range. */
start: number
/** Declared inclusive end seq of the replaced surface range. */
end: number
/** Actual surface entries removed by the operation, in surface order. */
shadowedSeqs: number[]
}
```
```ts type-equiv
/** Complete result of replaying the surface operations in a session log. */
interface SurfaceFoldResult {
/** Current surface event sequences in model-visible order. */
nodes: number[]
/** Replacement operations in event order. */
replacements: SurfaceFoldReplacement[]
}
```
## `Session` public API
去除方法体的声明与源码中的普通类保持同步,覆盖其公共构造函数、状态访问器、追加边界和历史投影。存储操作仍由生成的 [`ctx.sessions` 服务目录](../cordis-catalog/services.md#ctxsessions--sessionstore)记录。
```ts public-api
/**
* An event-sourced session: an append-only log of {@link SessionEvent}s.
*
* Plain class (not a Service) — create instances via `ctx.sessions.create()`.
* Seeding with an existing event log replays/forks a session.
*/
declare class Session {
/** The ordered surface over this session's event log. */
get surface(): SessionSurface;
/**
* Detached, deep-frozen creation metadata (format version, cwd, lineage,
* seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
* `Session` is constructed bare (tests, ad-hoc replay), a minimal header is
* synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
* `session.header` is always present. Kept out of the event log — it is a
* storage concern, not replayable conversation state.
*/
readonly header: SessionHeader;
/** The session identity, derived from its durable header's single copy. */
get id(): SessionId;
constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);
/**
* An immutable snapshot of the append-only event log. The snapshot is reused
* until the next append; a previously returned array does not grow later.
* Events and their nested data are deep-frozen at acceptance, so neither a
* cast nor ordinary JavaScript can rewrite durable history.
*/
get events(): readonly SessionEvent[];
/** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
get seq(): number;
/**
* Append one typed event to the log and synchronously notify observers via
* the store-owned, module-private publication hooks. The hot path never blocks
* on I/O — persistence plugins buffer asynchronously. Once the event enters
* the log, the append is committed: observer failures are logged and
* contained per listener, so they do not change the return value or prevent
* later listeners from observing the same accepted event.
*
* @param type - The event type (key of {@link SessionEventMap}).
* @param data - The event payload; must be JSON-serializable.
* @param opts - Surface metadata: `surfaceOp` controls how the event enters
* the ordered surface; `sourceEventSeqs` records provenance (the seq
* numbers of events this one derives from). REQUIRED for
* {@link SurfaceEventType} events (every message-producing event must
* declare how it joins the surface, the sole source of derived history) and
* rejected by the compiler for non-surface types like `turn/start` or
* `assistant/chunk`.
* @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
* `data` that entered the log, so reading `event.data` back sees the logged
* value, never the caller's still-mutable input.
* @throws if `data` or surface metadata is not losslessly JSON-serializable
* (BigInt, function, symbol, undefined, negative zero, non-finite number,
* circular reference, sparse array, or an exotic object such as
* Map/Set/Date/class instance), or when the candidate violates the
* canonical surface contract (marker shape and eligibility, unique
* earlier provenance, positional replacement validity, and complete
* shadowed-node coverage). One recursive pass reads, validates, and
* copies each nested value once, so a stateful getter cannot supply one value
* to validation and another to storage. The event log is the durable source
* of truth, so a bad event fails at the append site rather than later during
* a backend flush. A synchronous internal dispatch validation failure or an
* append reentered while this acceptance/publication boundary is open also
* rejects before the log changes.
*/
append<T extends SessionEventType>(
type: T,
data: SessionEventMap[T],
...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : []
): SessionEvent<T>;
/**
* The {@link EpochHeader} in force after the log's last header event — the
* header the NEXT request will be compared against — or undefined before
* the first `request/header` snapshot. The live, incrementally-maintained
* form of `foldRequestHeader(session.events)`: each header event is folded
* once, when first seen, so a per-step read costs O(new events).
* @returns the folded header, or undefined when no header event exists yet.
*/
requestHeader(): EpochHeader | undefined;
/**
* Derive the LLM message history by walking the ordered sequences of
* message-producing events maintained by `surfaceOp` markers. The
* surface is the single source of derived history: every message-producing
* append records its `surfaceOp`, so a raw event with no marker (a chunk, a
* turn boundary) is correctly absent, and a compaction `replace` deletes the
* shadowed nodes from the derivation. The projection rules are
* {@link deriveEventMessage}, folded per node.
*
* CACHED: each surface node is projected exactly once, when first seen — a
* call costs O(new nodes), and a surface rewrite (a `replace`;
* {@link SessionSurface.replaceGeneration}) rebuilds. The returned array is
* a fresh snapshot per call (later appends never grow an array a caller
* already holds); the `Message` objects in it are SHARED and **deep-frozen**.
* Their content reuses the already frozen durable event data, so the cache
* needs no second deep clone and consumers still cannot mutate the log.
* @returns a fresh array of the shared, frozen derived history.
*/
deriveMessages(): Message[];
/**
* Project a single event into the LLM message it derives to, or null when
* it produces none — a non-surface event (chunk, boundary, log-only record)
* or an empty-content assistant/message (which exists only to host usage).
* The per-node pure function {@link deriveMessages} folds over the surface;
* an external reconstructor (or the dev invariant) folds the same function
* over a log prefix's surface to rebuild the exact messages any request was
* built from (the reconstructability Agent Note). The returned message wrapper is
* fresh; its content reuses the logged event's already deep-frozen durable
* data, so changing the wrapper cannot rewrite the log and changing content
* throws.
* @param event - the event to project.
* @returns the derived message, or null when the event produces none.
*/
deriveEventMessage(event: SessionEvent): Message | null;
}
```
## 派生历史:`deriveMessages()` 与 `deriveEventMessage()`
`Session.deriveMessages()` 将事件日志投影为模型看到的 `Message[]`。它是缓存的(每个 surface 节点在首次出现时投影一次surface 重写触发重建)且冻结的(每次调用返回一个新数组,引用共享的深冻结消息,因此通过投影修改已记录的历史在类型上不可表达)。`deriveEventMessage(event)` 是折叠所应用的逐节点纯函数,公开暴露以便外部重建器和开发不变式检查能以完全相同的规则投影日志前缀,不会与缓存产生分歧。投影规则:
- `user/message` → 一条携带确切 `content` 的 user 消息;可选 envelope 仅作为日志中的展示元数据保留。
- `assistant/message` → 一条 assistant 消息,包含事件的提供方/模型溯源信息和可选的适配器私有回放状态。原始 `assistant/chunk` 事件属于回放/UI 数据,在派生时会被**跳过**(组装后的消息才是权威)。**内容为空的** `assistant/message` 也会跳过:因 max-tokens 而截断且无内容的步骤仍会记录一条 `assistant/message` 以承载用量和溯源信息,但无内容的 assistant 轮次不得进入提供方 transcript。
- `tool/result` → 一条携带 `tool-result` 块的 user 消息。
- `user/message`(注入上下文,即非 `user` 来源)→ 按时间顺序在相应位置生成一条 user-role 消息,并原样承载其 `content`。可选的 JSON `meta` 保留在事件日志中,绝不渲染。
- `steering/message` → 按时间顺序在相应位置生成一条携带确切 `content` 的 user-role 消息;可选 envelope 仅作为日志中的展示元数据保留。
其余所有事件(`turn/*`、`step/*`、插件所有的 `llm/retry`均为结构信息不会投影为消息。token 记账读取每个步骤的 `assistant/chunk { type: 'usage' }` 记录;如果没有用量分片,则将 `assistant/message.usage` 作为已提交步骤的后备。失败的模型请求尝试没有 assistant 消息,因此其用量分片是持久化的记账记录。操作错误的步骤号记录在 `turn/end.reason``kind: 'error'`)中;如果是最终模型请求失败,其中包含规范化的 `LlmFailure` 事实,其他实时错误则包含消息/代码。由于这一尚未发布的格式有意不提供兼容性承诺seed/load 校验会拒绝缺少提供方和模型的请求头,以及缺少提供方/模型溯源信息的 assistant 消息,而不会猜测历史数据应走的提供方路由。
## 活跃会话 fork API
`ctx.sessions.create(id, { seed, meta })` 是底层的回放/fork 原语。对于普通的活跃会话 fork`SessionStore` 暴露一个策略 API
- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `boundary` seq为止的源事件默认为当前最后一个事件要求 boundary 事件必须是 `turn/end`,然后创建一个活跃的子会话,包含深克隆的种子事件和子会话元数据(`parentSession`、`seedLength` 及继承的 `cwd`)。
显式 `boundary` 允许调用者从之前完成的轮次 fork即使源会话有更新的事件或正在进行的轮次。API 拒绝非 `turn/end` 的 boundary而不是静默截断。更广泛的轮次封闭性检查留在既有的 `dsh-invariants` 插件和持久化修复路径中,不在 `fork()` 中重复。`dsh-subagent-fork` 保留其已完成前缀截断逻辑,因为工具时委托通常在父轮次仍然打开时启动;普通的会话分支应显式指定请求的 boundary。
## 轮次的触发原因:`TurnTriggerMap`
```ts type-equiv
/**
* What started a turn.
* Merge-extensible sum type (same pattern as MessageSourceMap).
*/
interface TurnTriggerMap {
message: { kind: 'message'; source: MessageSource }
/**
* An out-of-band context injection (`agent.inject()`) made while the agent
* was idle. The loop wraps the injected `user/message` (a non-`user` source,
* plugin by default) in a one-shot turn (`turn/start` → `user/message` →
* `turn/end`) so every event in the log stays turn-enclosed — the
* durability/replay boundary is the turn, and a bare event between turns would
* otherwise be indistinguishable from a crash tail on reload. The trigger's
* `source` mirrors that message's producer.
*/
injection: { kind: 'injection'; source: MessageSource }
}
```
<a id="why-a-turn-ended-turnendreasonmap"></a>
## 轮次的结束原因:`TurnEndReasonMap`
`aborted` 有意作为一种粗粒度的持久结果:它只记录取消中断了实时轮次,不记录是哪个运行时调用方发起取消。仅属于运行时的调用方词汇由 [`AgentCancelCause`](core.md#the-agent-handle) 定义;未来若有审计需求,应新增独立的控制请求事件,而非让终止结果承载这一信息。
```ts type-equiv
/**
* Why a turn ended. Merge-extensible sum type.
*/
interface TurnEndReasonMap {
completed: { kind: 'completed' }
/** A cancellation request interrupted the live turn. */
aborted: { kind: 'aborted' }
/**
* The turn failed: a step threw or the model reported a failure. `step` is the
* step number the failure occurred on (the operational error's location — the
* single durable record of an in-turn failure; live diagnostics also fire via
* `agent/error`). Final model-request failures retain their normalized facts
* as one `failure`; other turn failures retain their live Error message/code.
*/
error: { kind: 'error'; step: number } & (
| { failure: LlmFailure; message?: never; code?: never }
| { message: string; code?: string; failure?: never }
)
disposed: { kind: 'disposed' }
/** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
'max-tokens': { kind: 'max-tokens' }
/**
* Policy blocked the turn's claimed prompt before the first step. The
* zero-step turn still records a balanced durable boundary and veto reason.
*/
rejected: { kind: 'rejected'; reason: string }
/**
* A persistence backend closed a crash-orphaned turn on reload. The loop never
* emits this marker, and the events recorded before the crash remain intact.
*/
interrupted: { kind: 'interrupted' }
}
```
`max-tokens` 与模型调用中同名的 `FinishReason` 对应:只要轮次内有任何步骤以 `max-tokens` 结束,整个轮次就以 `max-tokens` 而不是 `completed` 结束(即使之后继续执行,截断事实仍优先),让消费方能够区分正常停止和截断停止;但它只优先于 `completed``disposed`/`aborted`/`error` 结果的优先级更高。`rejected` 表示一个零步骤轮次,其已认领的提示词被 `agent/prompt-submit` 钩子阻止ACPAgent Client Protocol桥接层将其映射为 `cancelled`)。`interrupted` 是唯一不会由任何 loop 发出的原因:它由崩溃恢复合成(见 [persistence.md](persistence.md))。两个 map 均可通过合并扩展。
## 轮次封闭不变式
每个会话事件都位于一个轮次**之内**(在 `turn/start` 和对应的 `turn/end` 之间。loop 在 `turn/start` *之后*追加已排队的 `user/message` 事件;空闲时的 `agent.inject()` 会用一次性的 `injection` 轮次包住其 `user/message`;没有打开的轮次时,`appendOutOfBand()` 同样会用一个轮次包住符合条件的仅日志事件。这使轮次成为唯一的持久性/回放边界:后端可以将最后一个 `turn/end` 之后的任何内容视为崩溃中断尾部,而不会丢失合法记录在轮次之间的上下文。可选的 `dsh-session/invariant` 配套插件通过 `ctx.invariants` 在开发环境中强制此不变式(消息事件若位于打开的轮次之外便会抛出)。见[轮次封闭不变式 Agent Note](../../.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md)。
## 插件贡献的仅日志事件
插件可以通过 declaration merging 添加额外的 `SessionEventMap` 类型。这些是**仅日志**事件:不是 `SurfaceEventType`(不携带 `surfaceOp`,不参与派生历史),但与所有事件一样,必须位于一个打开的轮次内。完整的逐事件枚举(核心与插件贡献的,含 payload 与溯源信息)见生成的[持久化日志事件目录](../persistence-catalog.md);压缩 seam 的 `compact/*` 语义在 [compaction.md](compaction.md) 中讨论。
钩子桥接层的 `hook/invoked` / `hook/result` 溯源对(来自 `@deepseek-ai/dsh-hook-protocol`)通过 `handlerId` 关联。轮次中间的钩子点(`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`)在 loop 已打开的轮次内触发,因此其 `hook/*` 记录天然位于轮次之内。`SessionStart` 不生成 `hook/*` 记录:它注入的 `user/message` 已是持久证据,而且当时没有已打开的轮次可容纳该记录(见[钩子桥接 Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md))。
## 持久性契约
持久化后端依赖的契约如下:持久日志无损保存每个事件,**包括** `assistant/chunk``seq` 必须连续,因此不能从规范日志中过滤分片。后端可以为事件批次选择自己的存储编码,只要 `load` 返回与追加时完全一致的事件即可JSONL 后端可选启用的打包分片行就是此类编码;见 [persistence.md](persistence.md))。所有 `event.data` 都必须可序列化为 JSON`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.events` 始终与后端可持久化的内容一致。新增携带不可序列化数据的事件类型,或破坏会话不变式配套插件所检查的轮次/步骤嵌套,会构成磁盘格式的破坏性变更。
消费此契约的后端见 [persistence.md](persistence.md)。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
skills.md: fc9599713dcfddec9719ed746b66ea0217b86cf5
skills.zh.md: 0eb4c0aa69ed56117c7508358c0d47e3b3e95fcb

View File

@@ -1,5 +1,7 @@
# Skills
English | [中文](skills.zh.md)
The [skill capability family](../../packages/skill) is split across three packages: the registry ([dsh-skill](../../packages/skill/skill), `ctx.skills`) merges provider catalogs; the local provider ([dsh-skill-local](../../packages/skill/skill-local)) scans project/custom/user directories; the consumer ([dsh-tool-skill](../../packages/skill/tool-skill)) owns the session-prefix catalog and model-facing `skill` tool. Skills are optional instructions, not session events, so their vocabulary lives here rather than in [core.md](core.md).
Source: [`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts), [`packages/skill/skill-local/src/index.ts`](../../packages/skill/skill-local/src/index.ts), and [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts).

View File

@@ -0,0 +1,159 @@
# Skills
[English](skills.md) | 中文
[skill技能能力族](../../packages/skill)拆分为三个包package注册表[dsh-skill](../../packages/skill/skill)`ctx.skills`)合并各提供方的目录;本地提供方([dsh-skill-local](../../packages/skill/skill-local))扫描项目/自定义/用户目录;消费方([dsh-tool-skill](../../packages/skill/tool-skill))拥有会话前缀目录和面向模型的 `skill` 工具。skill 是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.md)。
源码:[`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts)、[`packages/skill/skill-local/src/index.ts`](../../packages/skill/skill-local/src/index.ts) 与 [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts)。
## 提供方注册表
`ctx.skills` 组合本地、内嵌、远程或其他提供方。注册是同步的;远程初始化与发现属于 `list()` 的 await 阶段。提供方对象、选项与候选项以只读方式借用,语义字段会被校验。
重名按 rank、提供方顺序、本地顺序依次解决摘要按名称排序。`list()` 拒绝时记录日志并跳过,不缓存降级后的目录;格式错误的候选项快速失败。
```ts type-equiv
/** Provider interface for one source of skills, such as local directories or a remote registry. */
interface SkillProvider {
/** Unique provider name in the `ctx.skills` registry. */
readonly name: string
/**
* List available skill candidates for the current lookup context. Provider
* plugins register synchronously during `apply()`; remote initialization,
* authentication, and discovery are awaited inside this method. Implementations
* should settle promptly when `options.signal` aborts.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @returns provider candidates with precedence ranks and opaque locators.
*/
readonly list: (options: SkillLookupOptions) => Promise<readonly SkillCandidate[]>
/**
* Load a complete skill body for a previously listed candidate.
* @param candidate - the winning candidate originally returned by this provider.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @returns the full skill body, or `undefined` if it is no longer loadable.
*/
readonly get: (candidate: SkillCandidate, options: SkillLookupOptions) => Promise<SkillDefinition | undefined>
}
```
## 本地发现优先级
内置的本地提供方按 rank 顺序扫描各根目录:
| Rank | Source | Root |
|---|---|---|
| 100 | `project-dsh` | `<projectRoot>/.dsh/skills` |
| 200 | `project-agents` | `<projectRoot>/.agents/skills` |
| 300 | `custom` | `Config.customSkillDirs` |
| 400 | `user-dsh` | `<dshHome>/skills` |
| 500 | `user-agents` | `<agentsHome>/skills` |
项目根目录为包含 `.git` 的最近祖先目录;找不到时使用当前 cwd。当 `ctx.fs` 可用时git-root 向上查找通过文件系统服务探测 `.git`,使远程或沙箱工作区不会回退到宿主文件系统边界。用户 DSH 根目录会跳过其 `.system` 子目录。本地提供方不附带内置系统 skill部署方通过另一个提供方提供内置 skill。
## Skill 身份
skill 名称为 kebab-case`^[a-z0-9]+(?:-[a-z0-9]+)*$`)。本地提供方接受目录包(`<name>/SKILL.md`)和扁平 Markdown 文件(`<name>.md`)。嵌套递归的 `**/SKILL.md` 发现有意不在 v1 范围内。
```ts type-equiv
/** Origin bucket for a skill contribution. The value is prompt-visible metadata, not precedence by itself. */
type SkillSource = 'project-dsh' | 'project-agents' | 'runtime' | 'user-dsh' | 'user-agents' | 'custom' | (string & {})
```
## 摘要、候选项与完整定义
`SkillSummary` 是注册表中可供模型调用的摘要形状。消费方自行选择渲染哪些字段;会话目录仅使用 `name` 和 `description`,从不使用 body 或绝对文件路径。`disableModelInvocation` 将 skill 从模型列表中隐藏,但允许受信代码按名称加载。
```ts type-equiv
/** Model-visible skill metadata returned by `ctx.skills.list()` and rendered into request guidance. */
interface SkillSummary {
/** Kebab-case identifier used with the `skill` tool. */
readonly name: string
/** Short routing description shown to the model. */
readonly description: string
/** Optional extra routing guidance shown to the model. */
readonly whenToUse?: string
/** Whether the skill is hidden from model listings while remaining loadable by trusted callers. */
readonly disableModelInvocation?: boolean
/** Discovery source that produced this winning skill. */
readonly source: SkillSource
/** Provider that owns this skill body. */
readonly provider: string
/** Provider-specific base for relative resources. */
readonly resourceBase?: SkillResourceBase
}
```
`SkillCandidate` 是提供方到注册表的形状。`locator` 是提供方的不透明状态;注册表只存储它并在调用获胜提供方的 `get()` 时传回。
```ts type-equiv
/** Provider catalog entry used by the registry to merge and later load skills. */
interface SkillCandidate extends SkillSummary {
/** Lower ranks win duplicate skill names before provider registration order is considered. */
readonly rank: number
/** Opaque provider-owned handle passed back to `provider.get()`. */
readonly locator: unknown
/** Absolute file path when the provider has one. */
readonly path?: string
/** Parsed optional metadata object from provider-specific skill frontmatter. */
readonly metadata?: Readonly<Record<string, unknown>>
}
```
`SkillDefinition` 是 `ctx.skills.get()` 返回的完整解析结果,供 `skill` 工具使用。`resourceBase` 告知工具如何为本地、URL 或提供方管理的 skill 渲染相对资源引导。
```ts type-equiv
/** Optional provider-specific base used by loaded skill bodies to resolve relative resources. */
type SkillResourceBase =
| { readonly kind: 'directory'; readonly path: string }
| { readonly kind: 'url'; readonly url: string }
| { readonly kind: 'opaque'; readonly description: string }
```
```ts type-equiv
/** Complete parsed skill definition, including the body loaded by `ctx.skills.get()`. */
interface SkillDefinition extends SkillSummary {
/** Markdown instruction body after any provider-specific metadata removal. */
readonly content: string
/** Absolute file path when the skill came from disk. */
readonly path?: string
/** Parsed optional metadata object from frontmatter. */
readonly metadata?: Readonly<Record<string, unknown>>
}
```
运行时 skill 使用相同的完整形状,参与相同的先到先得收集顺序。返回的 disposer 移除该贡献并使发现缓存失效。
```ts type-equiv
/** Runtime skill contribution accepted by `ctx.skills.register()`. */
type SkillRegistration = Omit<SkillDefinition, 'provider'> & { readonly provider?: string }
```
## 查找与配置
skill 查找对 cwd 敏感,因为提供方可能暴露工作区本地的 skill可选的 signal 为调用方取消提供方的工作。提供方接收与缓存标识和加载相同的只读选项对象。取消在目录选择前后(包括缓存命中时)都会检查,并与发现和完整定义加载竞争。如果找不到 git root本地提供方将所提供的 cwd 本身视为项目根目录。
```ts type-equiv
/** Caller context used for cwd-sensitive and abortable provider work. */
interface SkillLookupOptions {
/** Workspace selector for the current lookup. */
readonly cwd?: string | undefined
/** Abort discovery or loading work for the current caller. */
readonly signal?: AbortSignal | undefined
}
```
注册表只拥有其发现缓存上限。本地提供方拥有文件系统根目录(`dshHome`、`agentsHome` 与 `customSkillDirs`)。消费方拥有其目录描述上限。
```ts type-equiv
/** Skill registry configuration. */
interface Config {
/** Maximum number of completed cwd/provider catalogs kept in memory. */
readonly collectCacheMaxEntries?: number
}
```
## 会话目录与工具契约
`dsh-tool-skill` 通过 `agent/session-prefix` 贡献一条 user-role `<system-reminder>`。目录只包含已排序的 skill `name` 和规范化、经 XML 转义的 `description`不包含正文、路径、来源、提供方或路由提示。Prefix 发现通过 `SkillLookupOptions` 转发调用方的 abort signal。`catalogDescriptionMaxLength` 是消费方用于 description 上限的配置,默认值为 `500`,整数最小值为 `3`。其仅用于请求、记录在 header 中的生命周期由 [session-prefix Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-07-07-session-prefix.md)定义。
面向模型的 `skill({ name })` 工具校验 kebab-case 名称,为调用方 agent 的 cwd 加载完整定义,将未解析的 skill 报告为 unknown 或 no longer available拒绝 `disableModelInvocation` 的 skill并返回包含 `<skill_content name="...">`、`<skill_resources>` 和 `<skill_instructions>` 的工具结果。`resourceBase` 仅按需解析显式引用的脚本、参考资料和资产;加载结果不枚举 skill 目录。工具结果是模型获取完整指令的可见路径。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
subagent.md: 0335a3f0780ae17b57ae730f5a49a269261c8073
subagent.zh.md: dac48b624f6e0cfc28737e3e1a2774ba2d97e85b

View File

@@ -1,5 +1,7 @@
# Subagent
English | [中文](subagent.zh.md)
The subagent seam — an agent delegating work to a child agent. Like [bash](bash.md) it is **one optional capability**, not part of the agent-loop spine, so its vocabulary lives here rather than in [core.md](core.md). But it differs from every other seam on one axis: **multiple provider implementations coexist** in one context, registered by name (`ctx.subagents`), where bash allows only one executor. The registry shape mirrors the [LLM adapter registry](llm-streaming.md), not the single-service bash executor.
Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`); the model-facing consumer is [dsh-tool-subagent](../../packages/subagent/tool-subagent). The proposal and rationale: [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md).

View File

@@ -0,0 +1,248 @@
# Subagent
[English](subagent.md) | 中文
subagent seam一个 agent智能体将工作委派给子 agent。与 [bash](bash.md) 一样,它是**一项可选能力**,不属于 agent loop智能体循环主干因此其词汇定义在此而非 [core.md](core.md) 中。但它在一个维度上与其他所有 seam 不同:**同一上下文中可共存多个提供方实现**,按名称注册(`ctx.subagents`),而 bash 只允许一个执行器。注册表的形状参照 [LLM大语言模型适配器注册表](llm-streaming.md),而非单服务的 bash 执行器。
接口:[dsh-subagent](../../packages/subagent/subagent)`ctx.subagents` + 下文词汇。实现为三个兄弟包package`dsh-subagent-spawn``-fork``-acp`;面向模型的消费方是 [dsh-tool-subagent](../../packages/subagent/tool-subagent)。提案与设计理由见 [subagent Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)。
源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)
## 两类能力,两种发现方式
提供方通过一个静态描述符公布其**启动时**特性,服务在 run 存在之前即行检查;如果请求依赖提供方不具备的特性,会被大声拒绝(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不会被接受后静默忽略。**运行时**特性steering中途引导、恢复则是 [`SubagentRun`](#a-live-run-subagentrun) 上的可选方法——方法的存在即为能力TypeScript 的类型收窄即为发现机制。
```ts type-equiv
/**
* Which START-TIME features a provider supports. Checked by the service before delegating to
* {@link SubagentProvider.start}: a request that needs a capability the chosen provider lacks
* is rejected with a typed error rather than accepted-then-ignored (the "fail loud, no silent
* degradation" rule). These static flags cover features needed before a run exists; runtime
* capabilities such as steering and resume are optional {@link SubagentRun} methods whose presence
* is the capability.
*/
interface SubagentCapabilities {
/** Honor {@link SubagentStartRequest.outputSchema} (structured final output). */
readonly outputSchema: boolean
/** Enforce {@link SubagentStartRequest.maxDepth} (recursion cap). */
readonly depthLimit: boolean
/** Enforce {@link SubagentStartRequest.toolFilter} (child tool scoping). */
readonly toolFilter: boolean
/** Honor {@link SubagentStartRequest.persona} (a per-child persona). */
readonly persona: boolean
}
```
## 启动请求
工具层根据模型输入和自身配置构建此请求;服务在 `start` 之前针对指定提供方进行校验。必填的 `parent` 提供会话 cwd、谱系与委派深度。可选的 output schema、depth、工具过滤器和 persona 需要对应的能力 flag 匹配。不支持的 schema 在启动时即失败;进程内后端将 filter 和 persona 的作用域限定在子 agent 创建阶段,并通过强制 capture 工具实现所支持的 object-rooted schema。
```ts type-equiv
/**
* What a caller asks for when starting a subagent. The tool layer builds this
* from the model's `{ description, prompt }` plus its own config; the service
* validates {@link SubagentCapabilities} against the named provider, then
* passes it to {@link SubagentProvider.start}.
*/
interface SubagentStartRequest {
/** The task/prompt for the child agent (a user message in the child session). */
readonly prompt: ContentBlock[]
/**
* The spawning ("parent") agent — the one whose tool call started this
* subagent. REQUIRED: in-process backends read `parent.session.header` for
* the working directory, the `parentSession` lineage to stamp on the child,
* and the parent's delegation depth. The out-of-process backend (ACP) reads
* exactly one field — the session header's cwd, the child's workspace when
* no deployment `cwd` override is configured; nothing else crosses the
* process boundary.
*/
readonly parent: Agent
/**
* Cancellation signal from the spawning context (the tool's `exec.signal`).
* This is the canonical cancellation channel both before and after startup:
* a provider rejects `start()` after cleaning partial resources when it
* fires before publication, and cancels a published child when it fires
* afterward.
*/
readonly signal: AbortSignal
/** Per-child agent options (model and plugin-defined extension fields). */
readonly agentOptions?: AgentOptions
/**
* Object-rooted JSON Schema within `assertObjectJsonSchema`'s enforced subset. Start rejects
* unsupported schemas or providers without the capability. Data must be plain host-realm JSON;
* a successful child returns the matching value as {@link SubagentResult.structured}.
*/
readonly outputSchema?: ObjectJsonSchema
/**
* Optional absolute delegation-depth cap for the child being started: its
* computed depth must be less than or equal to this non-negative safe
* integer. Requires {@link SubagentCapabilities.depthLimit}; rejected at
* start otherwise.
*/
readonly maxDepth?: number
/**
* Optional child tool scoping. Requires {@link SubagentCapabilities.toolFilter};
* rejected at start otherwise. In-process backends apply it as a scoped
* `tools.restrict()` in the child's creation window: the named tools vanish
* from the child's prompt AND refuse to execute (one visibility), with loud
* unknown-name validation.
*/
readonly toolFilter?: ToolRestriction
/**
* Optional per-child persona. Requires {@link SubagentCapabilities.persona};
* rejected at start otherwise. In-process backends register it as a scoped
* `deployment:persona` section on the child, SHADOWING the deployment's
* persona for this child alone — same template semantics as the deployment
* persona (strict `{{…}}` interpolation against the registered variables).
*/
readonly persona?: string
}
```
`signal` 是就绪前后唯一的取消通道。[subagent 组合控制 Agent Note](../../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md)规定 persona、live 全局工具过滤、绝对深度以及「可见性而非权限」的设计理由。
## 终态结果:`SubagentResult`
一次 run 的最终产出,由 `SubagentRun.result` resolve。`structured` 仅在请求了 `outputSchema` 且成功满足时才存在;请求 schema 不保证一定能得到它,当子 agent 失败或结束时未产出有效 capture 时,提供方可能返回 `stopReason: 'error'`。非 `completed` 的 `stopReason` 意味着 `output` 可能不完整——消费方将其映射为 `isError` 的工具结果,而非将部分输出报告为成功。
```ts type-equiv
/**
* The terminal outcome of a subagent run, resolved by {@link SubagentRun.result}.
*/
interface SubagentResult {
/** The child's final assistant output (the last assistant message's content). */
readonly output: ContentBlock[]
/**
* The structured result after a requested `outputSchema` was successfully
* satisfied. Requesting a schema does not guarantee presence: a provider can
* end with `stopReason: 'error'` when the child fails or finishes without a
* valid capture. Shape is validated against the request schema by the
* provider; `unknown` here because the seam is schema-agnostic.
*/
readonly structured?: unknown
/** Why the run ended. A non-`completed` reason means `output` may be partial. */
readonly stopReason: SubagentStopReason
}
```
`SubagentStopReason` 是一个[可合并扩展的派生联合类型](core.md#the-map--derived-union-pattern)——后端可以添加变体,因此消费方应对已知 case 分支处理,将未知的终态原因视为失败:
```ts type-equiv
/**
* Why a subagent run ended. Merge-extensible (a backend may add variants);
* consumers branch on the known cases and fall through `default`. The known
* cases mirror the harness turn-end vocabulary so the tool layer can map a
* non-`completed` result to an `isError` tool result.
*/
interface SubagentStopReasonMap {
/** The child finished its turn normally. */
completed: 'completed'
/** The run was cancelled by its request signal or by disposal. */
aborted: 'aborted'
/** The child failed (model error, transport error). */
error: 'error'
/** The child hit its token ceiling before finishing. */
'max-tokens': 'max-tokens'
/** The child declined the task. */
refusal: 'refusal'
}
```
<a id="a-live-run-subagentrun"></a>
## 活跃 run`SubagentRun`
`SubagentRun` 是消费方持有的、指向一个就绪子 agent 的句柄。消费方 await `result` 并始终 dispose资源释放该 run直至其完全停稳。子 agent 失败时以非 completed 的 stop reason resolve只有不可表示的基础设施故障才会 reject。可选的 `sendMessage` 和 `resume` 方法通过自身的存在来公布运行时能力。
```ts type-equiv
/**
* Child handle returned only after readiness. Consumers await {@link result} and must always
* {@link dispose} to cancel remaining work and reach quiescence. Optional methods are runtime
* capability discovery; narrow their presence before calling.
*/
interface SubagentRun {
/**
* Parent-scoped run id. For a local run, this MUST equal the published child
* session id, whose `parentSession` records `request.parent.session.id`; a
* remote provider mints an id unique in the parent namespace.
*/
readonly id: SessionId
/**
* The exact published in-process child, or `undefined` for a remote run.
* When present, its id is {@link id}; the provider retains no ownership
* implication beyond the run's ordinary {@link dispose} contract.
*/
readonly localAgent: Agent | undefined
/**
* Resolves with the child's terminal {@link SubagentResult} when the run
* settles. Does NOT reject on a child-level failure — a model/transport
* failure resolves with `stopReason: 'error'` so the consumer maps it to an
* `isError` tool result. Rejects only on an infrastructure fault the seam
* cannot represent as a stop reason.
*/
readonly result: Promise<SubagentResult>
/**
* Cancel remaining work, reach child quiescence, and release the run's
* resources (in-process: dispose the owned agent and remove its session;
* ACP: kill and reap the subprocess). Idempotent.
*/
dispose(): Promise<void>
/**
* OPTIONAL (steering capability): send additional content to the running
* child between steps. Present only on providers that support live steering.
*/
sendMessage?(content: ContentBlock[]): void
/**
* OPTIONAL (resume capability): send a follow-up task to a settled child,
* continuing its session, and return a fresh run for the continuation.
*/
resume?(content: ContentBlock[]): Promise<SubagentRun>
}
```
本地 run 必须在 `start()` fulfill 前发布一个普通子 agent/会话,将该子会话 id 作为 `SubagentRun.id` 返回,以 `localAgent` 暴露确切子 agent并在子 agent 的 `parentSession` header 中记录 `request.parent.session.id`。运行时所有权可以把子 agent 放在 parent、提供方或 root 作用域下。远程提供方则返回 parent 作用域的生命周期 id 与 `localAgent: undefined`。
## 提供方 seam`SubagentProvider`
每个提供方是一个具名的子 agent 传输层,多个提供方可以共存。服务在 `start()` 之前校验请求的启动时能力。`inheritsParentContext` 仅描述对话种子注入(`fork`true`spawn` 和 `acp`false使消费方能生成准确的面向模型的措辞而不暗示继承了工具、服务或权限。
```ts type-equiv
/**
* A subagent backend: one transport for running a child agent (in-process
* spawn/fork, ACP to another process, …). Implementations register under a
* unique name via {@link SubagentService.registerProvider}; multiple providers
* coexist in one context (unlike the single-implementation bash seam). The
* Providers are trusted same-process implementations; callers treat their
* descriptors and returned values as borrowed immutable data.
*/
interface SubagentProvider {
/** Unique registry name (e.g. `spawn`, `fork`, `acp`). */
readonly name: string
/** The start-time features this provider supports (see {@link SubagentCapabilities}). */
readonly capabilities: SubagentCapabilities
/**
* Whether the child sees the parent's completed-turn prefix. This is descriptive, not a
* service-validated start capability: the model-facing tool derives truthful wording from it.
* It says nothing about tool registration, injected services, or authority inheritance.
*/
readonly inheritsParentContext: boolean
/**
* Establish a child and return its handle only after publication. The
* service has already validated that every requested start-time capability
* is supported, so an implementation may assume e.g. `request.maxDepth` is
* honorable when present. If setup fails or `request.signal` aborts before
* fulfillment, the provider owns and cleans all partial resources before this
* promise rejects. Ownership transfers to the caller only on fulfillment.
*/
start(request: SubagentStartRequest): Promise<SubagentRun>
}
```
`start()` 仅在 run 就绪时 fulfill。服务铸造唯一 `runId`,从提供方的确切 `localAgent` 快照 `local`观察结果emit `subagent/start`,并返回同一个 runrejection 意味着提供方已清理,且不会 emit 生命周期事件对。配对的 `subagent/end` 携带相同标识与最终输出或基础设施失败。两个事件都仅用于观察,每个 listener 异常都会被独立隔离。
## 进程内后端:深度与种子
spawn 和 fork 后端通过 `parent.ctx` 创建一个普通 agent将取消信号传入核心创建流程并通过 `AgentHandle` 进行 dispose。移除提供方会阻止新的 start但不会撤销已接受的 run。每个子 agent 获得一个新的扁平作用域,而非继承父级注册。深度与 fork 种子注入复用既有的 agent 和会话词汇:
- **委派深度**由持久 `SessionHeader.delegationDepth` 与可合并扩展的运行时字段 `AgentOptions.subagentDepth` 共同表示;缺失表示顶层深度为零,存在的较大值具有权威性。两个字段都归该 seam 所有——循环既不设置也不读取它们——因此进程内子 agent 会持久保存 parent 深度 + 1恢复无法降低深度而且每次 start 都会拒绝超出安全整数域、或高于已定义绝对 `request.maxDepth` 上限的派生深度。
- **Fork 种子注入**使用 `CreateAgentOptions.seed`(一个 `SessionEvent[]` 前缀,经由 `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })` 传递,与 `resume` 使用的原语相同。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*——父级事件直到并包括其最后一个 `turn/end`——因此种子从 0 连续,[invariants](../../packages/support/invariants) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
system-prompt.md: 63a750c74300b4353f132d3dae9da52a10631f23
system-prompt.zh.md: 3f7ab9aee5743616f00e95e81fb9a3a4af2c6e8a

View File

@@ -1,5 +1,7 @@
# System Prompt Assembly
English | [中文](system-prompt.zh.md)
The [system-prompt package](../../packages/core/system-prompt) owns the data exchanged between prompt contributors and one assembly call. The package [README](../../packages/core/system-prompt/README.md) documents registration, ordering, scoping, and rendering behavior; this page pins the literal cross-package shapes that plugins implement or pass.
Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts).

View File

@@ -0,0 +1,62 @@
# 系统提示词组装
[English](system-prompt.md) | 中文
[system-prompt 包package](../../packages/core/system-prompt)负责管理提示词贡献者与一次组装调用之间交换的数据。该包的 [README](../../packages/core/system-prompt/README.md) 记录了注册、排序、作用域与渲染行为;本页固定各插件实现或传递的跨包字面形状。
源码:[`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)。
## 组装上下文
`AssembleContext` 标识一次组装所解析的作用域 layer并可携带该请求的显式控制 signal。它可合并扩展`dsh-agent` 添加可选的 live `agent` 字段,`assembleContextFor(agent, signal)` 则一起设置这些显式字段。裸组装既没有 scope也没有 signal。
```ts type-equiv
/** Merge-extensible context for one prompt assembly. */
interface AssembleContext {
/**
* Scope whose providers and waterfall listeners participate. When absent,
* only global providers and subject-less listeners participate.
*/
scope?: ScopeKey
/** Explicit control signal for the turn that requested this assembly, when any. */
signal?: AbortSignal
}
```
## 工具提供方结果
`ToolProviderResult.schemas` 是当前组装中对模型可见的工具集合。`knownNames` 是提供方在限制前的名称全集,用于区分「配置名拼写错误」与「已知工具在此作用域中被有意隐藏」。
```ts type-equiv
/** Tool schemas visible in one assembly and their pre-restriction name set. */
interface ToolProviderResult {
/** The schemas this provider contributes to THIS assembly. */
readonly schemas: readonly ToolSchema[]
/** The pre-restriction name universe for config validation (defaults to `schemas`' names). */
readonly knownNames?: readonly string[]
}
```
## 提示词段落
`PromptSection` 是一份只读的同进程注册契约。其文本可以是静态的,也可以从当前组装上下文动态解析。
```ts type-equiv
/** One contributed section of the system prompt (registry input). */
interface PromptSection {
/** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */
readonly name: string
/**
* Sections are concatenated in ascending order. Convention: `-100` is the
* harness identity, `0` the deployment persona, tool guidance uses 100199;
* other negative orders also render before the persona.
*/
readonly order: number
/**
* Static text or a provider evaluated at each assembly with that assembly's
* {@link AssembleContext}. The text may reference `{{variable}}`s — they are
* interpolated later, by {@link renderPrompt}.
*/
readonly text: string | ((context: AssembleContext) => string)
}
```

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
tools.md: 43d91e72d83dce0bec99417ca90f9394f4c2169e
tools.zh.md: b92f6d3d99fda7dc28d10556adcceee3946a9571

View File

@@ -1,5 +1,7 @@
# Tools
English | [中文](tools.zh.md)
The tool pipeline of [dsh-tools](../../packages/core/tools). [core.md](core.md) introduces `ToolDefinition` as the one pipeline-authoring type promoted to the spine and `ToolSchema` as the model-facing wire shape. This page owns the full `ToolDefinition`, the typed schema DSL that builds it, the guarded execution shapes, and the UI-presentation vocabulary.
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts) · [`packages/core/tools/src/schema.ts`](../../packages/core/tools/src/schema.ts) · [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts)

View File

@@ -0,0 +1,414 @@
# 工具
[English](tools.md) | 中文
[dsh-tools](../../packages/core/tools) 的工具流水线。[core.md](core.md) 介绍了 `ToolDefinition`(唯一被提升到主干的流水线编写类型)和 `ToolSchema`面向模型的协议格式wire format形状。本页拥有完整的 `ToolDefinition`、用于构建它的类型化 schema DSL、受保护的执行形状以及 UI 展示词汇。
源码:[`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts) · [`packages/core/tools/src/schema.ts`](../../packages/core/tools/src/schema.ts) · [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts)
## `ToolDefinition` — 一个已注册的工具
由一个 `ToolSchema`(面向模型的字段)、必需的规范输出声明、`execute` 函数、仅供宿主使用的调度器元数据、可选的最终内容回调和可选 UI 展示函数组成。注册表持有这些定义,循环通过它们分派调用。注册表的 `schemas()` 通过显式允许列表构建面向模型的 `ToolSchema[]``output`/`execute`/`finalizeContent`/`timeoutMs`/`isConcurrencySafe`/`presentCall`/`presentResult` 绝不能泄漏到模型请求中。
```ts type-equiv
/** Tool-owned canonical output contract used after the body returns a JSON value. */
interface ToolOutputDefinition {
/** Raw supported JSON Schema enforced against every successful canonical value. */
readonly schema: JsonSchemaNode
/** Pure projection from validated arguments and value to Native/model content. */
render(args: unknown, value: JsonValue): ContentBlock[]
/** Pure replayable presentation projection, computed only for surface calls. */
presentationMeta?(args: unknown, value: JsonValue): JsonValue
}
```
```ts type-equiv
/** A registered tool: its schema plus the execution function. */
interface ToolDefinition extends ToolSchema {
/** Mandatory canonical output declaration. */
readonly output: ToolOutputDefinition
/**
* Run one accepted call and return only its canonical lossless-JSON value.
* Async work must observe or forward `exec.signal` and settle only after its
* owned work reaches quiescence. The registry preserves caller cancellation
* through around-dispatch signal replacement and does not abandon this
* promise, but it cannot hard-kill same-process code.
* @param args - losslessly snapshotted, frozen model arguments.
* @param exec - execution identity, cancellation signal, and context deferral.
* @returns the canonical value declared by `output.schema`.
*/
execute(args: unknown, exec: ToolRunContext): Promise<unknown>
/**
* Synchronous last-mile transform for model-facing content. The registry
* snapshots this callback when execution starts and invokes it exactly once
* for every normalized outcome, including pipeline failures that bypass
* `tools/post-execute`, immediately before lossless materialization.
* Returning `undefined` preserves the content; every other result field
* remains registry-owned. The callback must be total and must not throw.
* @param exec - immutable execution identity and arguments.
* @param result - complete normalized outcome before materialization.
* @returns replacement content, or `undefined` to preserve it.
*/
finalizeContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
/**
* Cooperative tool-call timeout budget in milliseconds. Omit for no deadline.
* Enforced by `@deepseek-ai/dsh-timeout-policy` (a `tools/execute` wrapper); it
* is NEVER sent to the model — `schemas()` whitelists only name/description/
* parameters. Declaring it asserts this tool forwards `exec.signal` to a
* cooperative implementation that can reach quiescence when the signal aborts.
*/
timeoutMs?: number
/**
* Pure synchronous classifier for overlap with sibling tool calls. Only
* `true` opts in; omission, exceptions, non-`true` returns, and invalid
* `defineTool` arguments are exclusive. This metadata is never model-visible.
*
* Opted-in executions must not mutate parent-owned state. Shared state must
* tolerate concurrent dispatch; recorder races are permitted only when they
* commute or fail closed. See the
* [parallel-tool-call Agent Note](../../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)
* for the full contract.
* @param args - parsed arguments; `defineTool` validates before calling.
* @returns Whether this call may join a parallel group.
*/
isConcurrencySafe?(args: unknown): boolean
/**
* Optional: how to present the PENDING state of one call in a UI, derived from
* the call's `args` (parsed arguments, `unknown` — the tool validates/narrows
* its own input). Returns a {@link ToolCallView} (a `card`-tagged render intent),
* or `undefined` (or omit the method) to fall back to a generic presentation
* (title = tool name, raw args as input). Pure and side-effect-free: a UI may
* call it during live streaming AND a session-log replay, so it must depend
* only on `args`.
*/
presentCall?(args: unknown): ToolCallView | undefined
/**
* Optional: how to present the COMPLETED state, given the same `args` and the
* durable result projection (`content`, failure state, and optional `meta`). Returns a
* {@link ToolResultView}, or `undefined` (or omit the method) to keep the
* pending title and render the raw result content. Pure and side-effect-free
* for the same replay reason.
*/
presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined
}
```
`execute` 接收 `args: unknown`——原始的 `ToolDefinition` 自行校验输入。第一方工具不需要手写校验;它们使用 `defineTool`,由后者代为校验并收窄参数类型、根据 `output.schema` 推导函数体返回类型,并为两个输出投影器提供类型约束。`finalizeContent` 特意接收不可变的执行对象而非类型化参数,因为无效输入和外层流水线失败也会到达该回调;它可以施加工具自有的内容限制,同时保留 `isError`、规范值、结构化错误身份、延迟上下文与展示元数据。
## 统一的 JSON 值 schema DSL
插件作者使用同一套词汇描述类型化参数和类型化输出值。`ValueSchemaSpec` 支持 `string`、`number`、`integer`、`boolean`、`null`、`array`、`object`、仅作者侧可用的 `json`,以及要求恰好命中一个分支的 `oneOf`;标量 `enum` 和 `const` 值必须与节点类型匹配。显式对象节点始终声明 `additionalProperties: true | false`。参数定义仍是隐式的开放对象属性映射,每个必填属性都附带 `required: true`。
源码:[`packages/core/tools/src/schema.ts`](../../packages/core/tools/src/schema.ts)
```ts type-equiv
/** One author-facing schema for any lossless JSON value root. */
type ValueSchemaSpec =
| StringValueSchemaSpec
| NumberValueSchemaSpec
| IntegerValueSchemaSpec
| BooleanValueSchemaSpec
| NullValueSchemaSpec
| ArrayValueSchemaSpec
| ObjectValueSchemaSpec
| JsonValueSchemaSpec
| OneOfValueSchemaSpec
```
```ts type-equiv
/** One implicit parameter-root property, optionally required. */
type ParameterPropertySpec = ValueSchemaSpec & { required?: true }
```
```ts type-equiv
/**
* Tool parameter schema. The map itself is an implicit open object root;
* requiredness remains a per-property `required: true` annotation.
*/
type ParameterSchemaSpec = {
[key: string]: ParameterPropertySpec
[key: symbol]: never
}
```
`{ type: 'json' }` 推导为 `JsonValue`,并编译成仅含注解、不施加约束的原始 schema。输出根可以是对象、数组、标量或 null。`InferValue<S>` 在 16 层容器内保留字面量约束与对象开放性,之后回退为 `JsonValue`,避免耗尽 TypeScript 的类型实例化栈。`InferArgs<P>` 依据逐属性的必填标记生成必填和可选的字符串键:
```ts type-equiv
/**
* Infer the TypeScript value accepted by an author-facing value schema. Exact
* inference is bounded to 16 container levels, then falls back to `JsonValue`.
*/
type InferValue<S> = InferValueAt<S, []>
```
```ts type-equiv
/** Infer the TypeScript argument object for an implicit parameter schema. */
type InferArgs<S> = InferProperties<S, []>
```
`defineTool({ name, description, parameters, output, execute, … })` 将参数推导与 `parameterSchemaSpecToJsonSchema()` 和 `validateArgs()` 绑定,并将 `execute`/`render`/`presentationMeta` 与 `InferValue<OutputSchema>` 绑定。Schema 记录只包含自有且可枚举的字符串键schema 数组是稠密的内建数组,因此推导、编译与校验观察到的是同一份声明。精确推导保持到 16 层容器,之后放宽为 `JsonValue`;运行时校验仍会继续遍历完整 schema。`valueSchemaSpecToJsonSchema()` 通过同一套已强制执行的原始子集编译输出声明。参数不匹配时抛出 `ToolArgsError``INVALID_ARGS`);函数体或后置策略产生的值无效时抛出 `ToolOutputError``INVALID_TOOL_OUTPUT`)。两者都经由常规工具错误路径处理。原始 JSON Schema 默认保持开放;不支持的关键字会被拒绝,而不会在未强制执行的情况下获准进入。
注册是一个受信任的同进程契约。注册表以 readonly 输入借用类型化定义,要求它声明 `output`,校验其原始 schema并检查 `timeoutMs` 必须为正有限值等语义要求;`schemas()` 在模型边界处物化显式的面向模型投影,使执行和展示共享同一份已解析定义,而不会将回调泄漏到协议上。
## `ToolRestriction` — 单个作用域的实时全局过滤器
`ToolRestriction` 仅作用于实时的部署全局工具层。注册表将 readonly 名称编译为私有集合,对多个限制取交集,再叠加作用域本地工具。仅 deny 的过滤器允许后续未列出的全局工具通过,而 allow 列表则排除它们。
```ts type-equiv
/**
* Per-scope filter over global tools. Restrictions intersect and do not affect
* scoped registrations or the reserved Code Mode transport.
*/
interface ToolRestriction {
/** Global tool names that stay visible; everything else is removed. */
readonly allow?: readonly string[]
/** Global tool names removed from visibility. */
readonly deny?: readonly string[]
}
```
## 执行:可扩展的 waterfall瀑布式事件加单调策略
`ctx.tools.execute()` 接受由调用方拥有且包含必需 readonly `signal` 的 `ToolExecutionInput`,将其解析后的 JSON 参数一次性物化为流水线拥有的 `ToolExecution`,然后让调用依次经过 `tools/pre-execute`(可重排的 allow/deny/ask waterfall→ 已注册的单调 guard → `tools/execute`(环绕分派包装层)→ `tools/post-execute`(检查/替换结果)→ 可选且由定义拥有的 `finalizeContent` → `tools/result`(不可变的权威结果)。只有 `tools/execute` 视图可以替换必需的 signal。最终产出为 `ToolExecutionResult`。
```ts type-equiv
/** Opaque call identity that permits correlation without exposing mutable execution state. */
type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true }
```
```ts type-equiv
/**
* Caller-supplied description of one tool call. {@link ToolRegistry.execute}
* adds the registry-owned token to form a pipeline {@link ToolExecution};
* callers do not choose that token.
*/
interface ToolExecutionInput {
readonly callId: CallId
readonly name: string
/** Losslessly JSON-serializable parsed arguments (tools validate their own schema). */
readonly arguments: unknown
/** The agent on whose behalf the call runs (set by the agent loop). */
readonly agent?: Agent
/**
* Opaque token of the enclosing transport execution, when one exists. Code
* Mode sets this on SDK sub-dispatches so commit-style observers can wait for
* the outer `run_code` outcome without receiving its live mutable execution.
*/
readonly parent?: ToolExecutionToken
/** Required caller-owned cancellation for this invocation. */
readonly signal: AbortSignal
}
```
工具函数体接收运行时扩展。`deferContext()` 是组合工具的通道:它记录嵌套分派产生的上下文,而不会在外层调用尚未结束时注入这些上下文。
```ts type-equiv
/**
* Runtime context handed to a tool implementation after the registry has
* accepted a {@link ToolExecution}. A composite tool uses
* {@link deferContext} to ferry context produced by nested dispatches back to
* the outer result; the loop appends it only after the outer `tool/result`.
*/
interface ToolRunContext extends ToolExecution {
/**
* Defer one nested-dispatch context until this tool's final result reaches
* the agent loop. Contexts retain their individual source and metadata and
* are emitted in call order.
*/
deferContext(context: HookContext): void
}
```
agent loop智能体循环向注册表查询每个待处理调用的执行模式并据此形成独占屏障和滚动池并行执行
```ts type-equiv
/**
* Scheduling mode for one pending call. `parallel` may overlap with siblings;
* `exclusive` runs alone and forms an ordering barrier.
*/
type ToolExecutionMode =
| { kind: 'parallel' }
| { kind: 'exclusive' }
```
```ts type-equiv
/**
* One pending tool call inside the registry pipeline. Parsed arguments cross
* one lossless-JSON materialization boundary before policy and are deep-frozen;
* call identity, the caller signal, and the registry-assigned {@link token} are
* readonly. The registry freezes the complete object before `tools/result`
* observers run.
*/
interface ToolExecution extends ToolExecutionInput {
/** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */
readonly token: ToolExecutionToken
}
```
```ts type-equiv
/**
* Around-dispatch view of a {@link ToolExecution}. A `tools/execute` wrapper
* may replace the signal for its delegated lifetime, but it cannot remove it.
* The registry fuses every replacement with the captured caller signal.
*/
interface ToolDispatchExecution extends Omit<ToolExecution, 'signal'> {
/** Cancellation signal visible to the next wrapper or tool body. */
signal: AbortSignal
}
```
`ToolExecutionToken` 是不透明的运行时 `Symbol`,仅用于身份比较。策略执行前,`execute()` 会物化并冻结参数、拒绝非 JSON 输入并分配 token。身份字段、调用方必需的 signal 和可选的 parent token 均保持 readonly。`ToolDispatchExecution` 包装层可以替换 signal 但不能移除;注册表会在调用工具函数体前重新融合调用方的 signal。最终观察者接收冻结的执行身份。
`ToolGuard` 是感知作用域的最终预分派策略。其形状有意不包含 allow 结果:`undefined` 保留 waterfall 的决策,而返回的 reason 只能缩减权限,因此后续监听器无法撤销它。
```ts type-equiv
/**
* A monotonic execution guard evaluated after every `tools/pre-execute`
* listener and before the tool body. Returning a reason denies the call;
* returning `undefined` leaves it unchanged. Because guards have no allow
* result, listener ordering cannot turn a denial back into permission.
* @param execution - the identity-protected call after extensible pre-execute policy completed.
* @returns a final denial reason, or `undefined` to leave the call allowed.
*/
type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefined
```
```ts type-equiv
/** Canonical failure detail; internal routing information remains optional. */
interface ToolFailure {
/** Human-readable failure message without the Native `Error: ` envelope. */
message: string
/** Internal error class/code used by policy and durable diagnostics. */
info?: ToolErrorInfo
}
```
```ts type-equiv
/** Successful canonical tool execution, including its Native/model projection. */
interface ToolExecutionSuccess {
readonly isError: false
/** Execution-local canonical value; deliberately omitted from durable events. */
readonly value: JsonValue
readonly content: ContentBlock[]
readonly error?: never
readonly meta?: JsonValue
readonly additionalContexts?: HookContext[]
}
```
```ts type-equiv
/** Failed canonical tool execution; failures never carry a successful value. */
interface ToolExecutionFailure {
readonly isError: true
readonly error: ToolFailure
readonly value?: never
readonly content: ContentBlock[]
readonly meta?: JsonValue
readonly additionalContexts?: HookContext[]
}
```
```ts type-equiv
/** The discriminated, execution-local outcome of one tool call. */
type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure
```
结果仅承载产出。调用身份保留在不可变的 `ToolExecution` 上,后者伴随结果经过每个钩子,并出现在持久化的 `tool/call` / `tool/result` 会话事件上,因此包装层无法创建第二个相互矛盾的身份。规范的 `value` 仅存在于执行期间:循环只持久化 `content`、`error` 和 `meta``tool/code-dispatch` 则存储有界摘要。回放可以重现展示,却无法重建中间值。
成功时,注册表会快照并校验函数体返回值,将其冻结,然后调用纯渲染器;对于直接的外层调用,还会调用可选的元数据投影器。注册表会在 `tools/result` 之前另行物化持久展示字段;无效值、渲染器/投影器失败或非 JSON 展示都会转为 JSON 安全的 `isError`。因此,最终实时观察者能看到精确的执行期值,以及可安全用于后续持久追加的字段。
在得到最终内容之前,注册表会物化候选结果;若内容、结构化错误、附加上下文或展示元数据无法物化,则会转为仍可到达 `finalizeContent` 的 JSON 安全 `isError` 结果。注册表恰好调用该回调一次,随后在 `tools/result` 之前立即物化并冻结已接受的结果,因此实时观察到的产出可安全用于后续持久化的 `tool/result` 追加。
每个拦截 waterfall 返回一个类型化的 **Decision**(与 `agent/*` seam 共享的惯用模式)。`tools/pre-execute` 监听器接收 `(exec, next)` 并返回 `PreToolDecision``tools/execute` 包装层返回 `ToolExecutionResult``tools/post-execute` 监听器接收 `(exec, result, next)` 并返回 `PostToolDecision`
```ts type-equiv
/**
* Pre-dispatch decision. `allow` runs the call; `deny` materializes an error;
* `ask` runs only after an approval service returns `allowed-once` and otherwise
* denies. Input rewriting is excluded because arguments are already logged and
* presented.
*/
type PreToolDecision =
| { kind: 'allow' }
| { kind: 'deny'; reason: string }
| { kind: 'ask'; reason?: string }
```
```ts type-equiv
/**
* Post-dispatch decision: accept, replace one projection, attach context for the
* next request, or block by turning corrective feedback into an error result.
*/
type PostToolDecision =
| { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: HookContext[] }
| { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: HookContext[] }
| { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: HookContext[] }
```
调用 `next()` 获取默认决策,或直接返回一个决策以短路。前置策略可以 deny 或 ask只有 `allowed-once` 才继续执行,而未授权、缺少审批通道或服务、或无 agent 的请求都会变为拒绝。Guard 仍可施加最终拒绝。参数不可被改写因为历史记录、审计、UI 和执行必须保持一致。
后置策略可以替换内容或值,但不能同时替换两者。替换内容会保留规范值和现有元数据;替换值会重新校验并重新计算内容/元数据;阻止会移除值,并转为包含纠正反馈的 `isError`。内容替换是展示策略,而非保密策略;需要隐藏程序化值的监听器必须阻止或替换该值。`tools/result` 在归一化后接收冻结的执行和结果;观察者无法对其进行变换,观察者的失败也会被隔离。未知工具和抛出异常的工具都会变为结构化错误(`ToolNotFoundError` 映射为 `UNKNOWN_TOOL`),调用失败但不终止当前轮次。
## 已强制执行的原始 JSON Schema 子集
subagent、工作流、MCP 和动态注册提供的原始 schema 使用作者侧 DSL 在协议层的对应表示。`assertSupportedJsonSchema()` 接受任意 JSON 根,`validateJsonSchemaValue()` 强制执行该 schema`JsonSchemaError` 则报告每条不受支持或格式错误的 schema 路径。仅含注解的空节点表示不受约束的无损 JSON。`oneOf` 至少要求两个分支,且一个值必须恰好匹配其中一个。仍要求对象根的消费方调用 `assertObjectJsonSchema()` 并携带 `ObjectJsonSchema`这样subagent/工作流中由调用方定义的结构化输出可以继续以对象为根,而不会限制共享词汇。
```ts type-equiv
/** Scalar JSON values supported by `enum` and `const`. */
type JsonSchemaScalar = string | number | boolean | null
```
```ts type-equiv
/** Single-type keywords accepted by the enforced subset. */
type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null'
```
```ts type-equiv
/**
* One raw JSON Schema node in the enforced subset. The optional fields express
* the external wire shape; {@link assertSupportedJsonSchema} rejects invalid
* combinations before a caller treats the node as trusted.
*/
interface JsonSchemaNode {
/** Omit with no constraints for any JSON value, or use `oneOf`. */
type?: JsonSchemaType
/** Exactly one branch must validate; at least two branches are required. */
oneOf?: JsonSchemaNode[]
/** Nested property schemas (`type: 'object'` only). */
properties?: Record<string, JsonSchemaNode>
/** Required property names; each must appear in `properties`. */
required?: string[]
/** `false` rejects undeclared keys; absent/`true` follows JSON Schema's open default. */
additionalProperties?: boolean
/** Item schema (`type: 'array'` only); absent accepts any JSON item. */
items?: JsonSchemaNode
/** Allowed values for a scalar node. */
enum?: JsonSchemaScalar[]
/** The single allowed value for a scalar node. */
const?: JsonSchemaScalar
/** Annotation, ignored for validation. */
description?: string
/** Annotation, ignored for validation. */
title?: string
/** Annotation, ignored for validation but required to be lossless JSON. */
default?: JsonValue
/** Annotation, ignored for validation but required to be lossless JSON. */
examples?: JsonValue
}
```
```ts type-equiv
/** A consumer-constrained object-rooted schema. */
type ObjectJsonSchema = JsonSchemaNode & { type: 'object' }
```
## 工具展示 UI 词汇
工具希望其调用在 UI 中如何呈现编辑器工具调用卡片、CLI命令行界面日志行提供方无关使工具在不依赖任何客户端协议的情况下描述自身。`presentCall`/`presentResult` 返回一个 **`card` 标签的渲染意图**——一个可辨识联合类型UI 桥接层据此分发:
- `ToolCallView`(待执行):`{ card: 'generic', title, kind?, rawInput?, content?, locations? }`(默认卡片;`locations` 是 `{ path, line? }[]`,表示调用读取/修改的文件,供编辑器跟随)、`{ card: 'terminal', title, description?, cwd? }`shell 命令→终端卡片)、或 `{ card: 'diff', title, diffs, locations? }`(文件创建/修改→行内 diff 卡片;`diffs` 是 `{ path, oldText, newText }[]`,新文件时 `oldText: null`)。
- `ToolResultView`(已完成):`{ card: 'generic', title?, content? }`、`{ card: 'terminal', title?, output?, exitCode?, signal? }`(捕获的运行输出 + 退出状态;有能力的 UI 显示退出状态标签,无能力的 UI 获得桥接层从 `output` 派生的围栏 ` ```console ` 回退)、或 `{ card: 'diff', title?, diffs }`(已完成的文件变更→要展示的变更,通常是从变更前后内容计算出带上下文行的已应用 hunk或在没有前像时的整文件 diff——例如文件创建。`tool_call_update` 的内容会替换调用的内容,因此变更工具即使与调用时的片段重复也要返回此卡片,以防结果文本覆盖 diff
`ToolCallKind``'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`)用于为通用卡片选择图标。`FileLocation``{ path, line? }`)与 `FileDiff``{ path, oldText, newText }`)是共享的文件卡片词汇。该设计由[渲染意图联合类型 Agent Noteagent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md)固定ACPAgent Client Protocol桥接层将 `diff` 卡片映射为 `{ type: 'diff' }` 内容块,将 `terminal` 卡片映射为 `_meta` 终端约定,并根据会话 cwd 将文件卡片标题转换为相对路径。
完整的展示字段文档见 [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts)。`bash` schema 与执行器见 [bash.md](bash.md);通用后台控制见 [tasks.md](tasks.md)。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
user-interaction.md: 7ddc2f46bdbbe25f2a8979b029d0c6b958f85b09
user-interaction.zh.md: 75b9b7795a36e0706672f0d17ba0b399c20cc644

View File

@@ -1,5 +1,7 @@
# User Interaction
English | [中文](user-interaction.zh.md)
The user-interaction seam of [dsh-user-interaction](../../packages/ui/user-interaction). It is the provider-neutral vocabulary a tool or permission plugin uses when it needs the human to answer before the agent can continue. UI surfaces provide the active `UserInteractionProvider`: `dsh-tui` uses keyboard-driven overlays, and `dsh-acp` maps questions to ACP form elicitations.
Source: [`packages/ui/user-interaction/src/index.ts`](../../packages/ui/user-interaction/src/index.ts)

View File

@@ -0,0 +1,108 @@
# 用户交互
[English](user-interaction.md) | 中文
[dsh-user-interaction](../../packages/ui/user-interaction) 的用户交互 seam。它是工具或权限插件需要人类回答后 agent智能体才能继续时所使用的、提供方无关的词汇。UI surface 提供活跃的 `UserInteractionProvider``dsh-tui` 使用键盘驱动的 overlay`dsh-acp` 则把问题映射为 ACPAgent Client Protocol表单 elicitation。
源码:[`packages/ui/user-interaction/src/index.ts`](../../packages/ui/user-interaction/src/index.ts)
## 问题选项
`AskUserQuestionOption` 是可选择项的形状。`label` 是面向用户的选项文字,同时也是面向模型的选中值;`description` 是可选的 UI 帮助文本。
```ts type-equiv
/** One selectable answer offered to the user. */
interface AskUserQuestionOption {
/** User-facing label. */
label: string
/** Optional extra context rendered by capable UIs. */
description?: string
}
```
## 问题条目
`AskUserQuestionItem` 是请求中的一个问题。调用方提供稳定的 `id`,它会随答案原样返回,使批量问题仍可路由。可选的 `detail` 携带辅助文本;提供方会将其随问题渲染,但不会放入可选 option label。
```ts type-equiv
/** One question in a user-interaction request. */
interface AskUserQuestionItem {
/** Stable caller-provided question id, echoed in the answer. */
id: string
/** The question to display. */
question: string
/** Optional supporting detail rendered with the question but kept out of option labels. */
detail?: string
/** Optional short heading/group label. */
header?: string
/** Optional choices the UI can render as a menu. */
options?: AskUserQuestionOption[]
/** Whether more than one option may be selected. Defaults to single-select. */
multiSelect?: boolean
}
```
## 提问请求
`AskUserQuestionRequest` 是跨包package的请求。`questions` 是数组,这样 UI 可以在一个流程中呈现相关提示,同时保持每个回答有稳定的 id。
```ts type-equiv
/** Request for a human answer. */
interface AskUserQuestionRequest {
/** Questions to display. */
questions: AskUserQuestionItem[]
/** Calling agent, when the request came from an agent tool call. */
agent?: Agent
/** Abort signal for the owning tool/step. */
signal?: AbortSignal
}
```
## 回答
提供方为每个问题 id 返回一个回答项。`selected` 包含选中的选项标签,`custom` 在用户输入自由文本时携带「其他」回答。当 `custom` 存在时,`selected` 为空自定义文本是对选中项的覆盖而非补充。UI 也可以使用 `selected` 为空且不含 `custom` 的回答项,在其余问题均已完成的批次中保留被跳过的问题。
```ts type-equiv
/** Answer to one question. */
interface AskUserQuestionAnswerItem {
/** The answered question id. */
id: string
/** Selected option labels. Empty for custom or unanswered choices. */
selected: string[]
/** Optional free-text "Other" answer. */
custom?: string
}
```
```ts type-equiv
/** The human's answer. */
interface AskUserQuestionAnswer {
/** Structured answers keyed by question id. */
answers: AskUserQuestionAnswerItem[]
}
```
## 提供方
同一上下文中只能有一个活跃的提供方。提供方注册绑定到 effect因此 HMR热模块替换或 dispose资源释放会移除当前活跃的 UI。
```ts type-equiv
/** UI-side provider for user questions. */
interface UserInteractionProvider {
ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
}
```
## 错误
`UserInteractionError` 继承 `HarnessError`,因此 `ctx.tools.execute()` 会保留 `{ name, code }`,用于面向模型的工具失败,如 `EMPTY_QUESTIONS`、`NO_PROVIDER`、`ASK_ABORTED` 或 ACP 侧取消。
```ts type-equiv
/** Stable error taxonomy for user-interaction failures. */
class UserInteractionError extends HarnessError {
constructor(message: string, code: string, options?: ErrorOptions) {
super(message, code, options)
this.name = 'UserInteractionError'
}
}
```

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
web.md: 20d07240c9d9fea2f1f5abbac810f349a3e81f9b
web.zh.md: 68ceed04bb0b80f32ed704118f1fc25f48a0da70

View File

@@ -1,5 +1,7 @@
# Web Access
English | [中文](web.zh.md)
The web access seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) that spans **two capabilities** (search and fetch) on one `ctx.web` service, split across packages: interface ([dsh-web](../../packages/web/web), `ctx.web` + the provider registries), implementations ([dsh-web-search-exa](../../packages/web/web-search-exa), [dsh-web-search-perplexity](../../packages/web/web-search-perplexity), [dsh-web-search-deepseek](../../packages/web/web-search-deepseek), [dsh-web-fetch-local](../../packages/web/web-fetch-local)), and consumer ([dsh-tool-web](../../packages/web/tool-web), the `web_search`/`web_fetch` tool schemas). Web is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A search-provider swap does not change how the model asks for a query, and a fetch-implementation swap does not change how the model asks for a URL.
Source: [`packages/web/web/src/types.ts`](../../packages/web/web/src/types.ts)

View File

@@ -0,0 +1,135 @@
# Web 访问
[English](web.md) | 中文
Web 访问 seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md),在同一个 `ctx.web` 服务上横跨**两项能力**search 与 fetch并拆分到多个包package接口[dsh-web](../../packages/web/web)`ctx.web` + 提供方注册表)、实现([dsh-web-search-exa](../../packages/web/web-search-exa)、[dsh-web-search-perplexity](../../packages/web/web-search-perplexity)、[dsh-web-search-deepseek](../../packages/web/web-search-deepseek)、[dsh-web-fetch-local](../../packages/web/web-fetch-local))与消费方([dsh-tool-web](../../packages/web/tool-web),即 `web_search`/`web_fetch` 工具 schema。Web 是**一项可选能力**,不属于 agent loop智能体循环主干因此其词汇定义在此而非 [core.md](core.md) 中。更换 search 提供方不会改变模型请求 query 的方式,更换 fetch 实现也不会改变模型请求 URL 的方式。
源码:[`packages/web/web/src/types.ts`](../../packages/web/web/src/types.ts)
## 为什么两项能力合为一个 seam
搜索与抓取既不共享请求 schema也不共享业务逻辑但它们被有意设计为同一个 `ctx.web` 中间层:一个提供方选择策略的所有者、一套 abort/error 词汇、一个面向产品的「此 harness 如何访问 Web」配置界面。代价是服务上并行的 `searchX`/`fetchX` 方法对;这种并行是有意为之,而非遗漏的提取。提供方注册的是**能力**`WebSearchProvider``WebFetchProvider`而非工具面向模型的名称、schema、提示词引导与展示全部集中在唯一的消费方 `dsh-tool-web` 中。
## 搜索请求与结果
面向模型的工具参数仅为一个 `query``maxResults` 是消费方自有的上限(`dsh-tool-web``searchMaxResults` 配置,默认 `8`),通过 seam 传递并在返回时强制执行——如果提供方返回超量seam 截断 `sources[]` 并设置 `truncated`
```ts type-equiv
/**
* What one search-capable backend can return. The model-facing argument is just
* a query; `maxResults` is a `dsh-tool-web`-layer bound passed through unchanged
* and enforced on the way back by the seam (see {@link WebSearchResult}).
*/
interface WebSearchRequest {
readonly query: string
/**
* Upper bound on returned sources; the seam truncates to it. Omitted = no
* bound. `dsh-tool-web` always sets it. A provider whose API supports a
* result-count control (Exa's `numResults`) should apply it at the request
* layer as a cost/latency optimization; the seam enforces the bound
* regardless.
*/
readonly maxResults?: number
}
```
```ts type-equiv
/**
* Normalized search outcome. `content` is optional provider-generated answer
* text or summary (Exa returns none; Perplexity returns a generated answer).
* `sources[]` is the portable citation surface. `truncated` is set by the seam
* when it cut `sources[]` down to `maxResults`.
*/
interface WebSearchResult {
/** Optional provider-generated answer text, search context, or summary. */
readonly content?: string
/** Citeable sources, already truncated to the request's `maxResults`. */
readonly sources: readonly WebSearchSource[]
/** True when the seam dropped sources to honor `maxResults`. */
readonly truncated: boolean
}
```
`content` 是提供方可选生成的回答文本Exa 和 DeepSeek 不返回Perplexity 返回生成式回答)。`sources[]` 是一套可跨提供方使用的引用数据结构。一个 source 必有 `url``title`/`snippet`/`publishedAt` 可选因为并非每个提供方都返回它们——Perplexity 的引用可能只有 URL强迫适配器编造其余字段会让 seam 说谎。`dsh-tool-web` 渲染时使用 `title ?? hostname(url)`。
```ts type-equiv
/**
* One citeable source. A source always has a URL; `title`, `snippet`, and
* `publishedAt` are optional because not every provider returns them — forcing
* adapters to invent them would make the seam lie (Perplexity citations may be
* URL-only). `dsh-tool-web` renders `title ?? hostname(url)` for display.
*/
interface WebSearchSource {
readonly url: string
readonly title?: string
readonly snippet?: string
/** Publication/crawl timestamp as a provider-supplied ISO-8601 string. */
readonly publishedAt?: string
}
```
## 抓取请求与结果
```ts type-equiv
/**
* What one fetch-capable backend is asked to retrieve. The request deliberately
* omits timeout, format, prompt, and extraction controls: cancellation is a
* direct execution argument, while presentation and higher-level LLM concerns
* belong outside safe retrieval.
*/
interface WebFetchRequest {
readonly url: string
}
```
HTTP 状态码是被抓取资源状态的一部分,不自动视为失败:成功的网络抓取返回 `404`/`500` 时,仍产出一个带状态码和有界解码 body 的 `WebFetchResult`。`url` 是经过允许的重定向后的最终 URL。`WebError` 仅用于无法安全获取或表示资源的情况。
```ts type-equiv
/**
* Normalized fetch outcome. A successful network fetch of a non-2xx response is
* a result, not an error: the status code is part of the fetched resource
* state. {@link WebError} is reserved for failures to safely retrieve or
* represent the resource.
*/
interface WebFetchResult {
/** The final URL after allowed redirects (the request URL is in the request). */
readonly url: string
/** HTTP status code of the fetched response. */
readonly statusCode: number
/** Decoded body, classified by content kind. */
readonly body: WebFetchBody
/** True when the provider capped the decoded body. */
readonly truncated: boolean
}
```
`WebFetchBody` 是 `dsh-web` 拥有的**封闭**可辨识联合类型(不是可合并扩展的 map提供方解码 kind`dsh-tool-web` 渲染它,因此新增一个 kind 是已知包之间的协调变更,而非插件扩展。消费方对 `kind` 做 `switch` 并以 `default: assertNever(...)` 结尾,所以新增 kind 会在每个消费方处编译失败,直到被处理。即使各分支当前字段一致,每个分支仍保持独立的对象字面量,为将来分支特有字段留出空间(例如未来 `pdf` body 的 `pageCount`)。
```ts type-equiv
/**
* The decoded body of a fetched resource. A CLOSED discriminated union owned by
* `dsh-web`: the provider decodes the kind and `dsh-tool-web` renders it, so a
* new kind is a coordinated change across known packages, not a plugin
* extension. Consumers `switch` on `kind` ending in `default: assertNever(...)`
* so adding a kind breaks compilation at every consumer until handled. Each arm
* stays its own object literal even where fields coincide today, leaving room
* for arm-specific fields later (a `pdf` body's `pageCount`).
*/
type WebFetchBody =
| { readonly kind: 'html'; readonly content: string }
| { readonly kind: 'text'; readonly content: string }
```
## 提供方可用性
提供方的 `available(): boolean` 是一个廉价的本地检查(凭证是否存在、配置是否可解析),**禁止发起网络调用**。它是执行时选择的输入,而非健康检查系统:`search()`/`fetch()` 读取它以选出可用的提供方,选择失败以结构化的 `WebError` 呈现给调用方路由——其 code 和 message 携带可分支的细节(缺失的 id 或有歧义的候选集)。
选择从不依赖注册顺序、配置顺序或 HMR热模块替换顺序一项能力要么有显式的提供方 id配置 `searchProvider`/`fetchProvider`,或填充同一字段的对应环境变量),要么在恰好只有一个可用提供方注册时自动选择;多个可用提供方且未配置 id 时为 `WEB_PROVIDER_AMBIGUOUS`,而非先注册先赢。
## 错误
`WebError extends HarnessError`[core.md](core.md) 错误分类体系),带有 `code: string`(开放式,与其他 seam 的错误一致——`LlmError`、`SubagentError`),而非封闭联合类型:提供方可以在不修改 `dsh-web` 的情况下抛出自己的 code消费方必须容忍未知 code。code 按所有者划分。seam 中立的 code 由 `WebService` 选择逻辑和共享契约抛出:`WEB_PROVIDER_UNAVAILABLE`、`WEB_PROVIDER_CONFIGURED_MISSING`、`WEB_PROVIDER_CONFIGURED_UNAVAILABLE`、`WEB_PROVIDER_AMBIGUOUS`、`WEB_DUPLICATE_PROVIDER`(注册时的编程错误,类似 `LlmService` 的 `DUPLICATE_ADAPTER`)、`WEB_ABORTED`,以及 `WEB_PROVIDER_ERROR`(提供方自身故障通过 seam 暴露的兜底 code包括网络/传输失败——DNS、连接被拒、TLS。抓取传输层 code 由 `dsh-web-fetch-local` 实现拥有,不同的抓取后端无需抛出它们:`WEB_INVALID_URL`、`WEB_BLOCKED_URL`、`WEB_REDIRECT_BLOCKED`、`WEB_FETCH_TOO_LARGE`、`WEB_FETCH_TIMEOUT`、`WEB_UNSUPPORTED_CONTENT_TYPE`。
## 服务
`WebService` 注册搜索与抓取提供方,以 `WEB_DUPLICATE_PROVIDER` 拒绝重复 id并在执行时以结构化的选择错误解析提供方。本地抓取后端仅接受 HTTP(S)、拒绝凭证、限制重定向次数、字节数、字符数和时间、对每一跳同源重定向重新校验,并解码 body展示由工具负责。私有网络阻断尚未实现因此请勿在可触及敏感内部目标的环境中启用 `web_fetch`。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
workflow.md: 8d271b89e71de6f6bef548aa8da61402ef9ada6e
workflow.zh.md: b8ed699eb52d9f0cef23c513f625de7e82c46c45

View File

@@ -1,5 +1,7 @@
# Workflow
English | [中文](workflow.zh.md)
The workflow seam — an agent running a model-written orchestration SCRIPT that fans out subagents. Like [subagent](subagent.md) it is **one optional capability**, not part of the agent-loop spine, so its vocabulary lives here rather than in [core.md](core.md). Unlike the subagent registry it takes the bash shape: ONE engine implementation per context provides `ctx.workflows`; there is no named-provider registry (a second engine is a plugin swap, not a co-resident).
Interface: [dsh-workflow](../../packages/workflow/workflow) (`ctx.workflows` + the vocabulary below). The implementation is [dsh-workflow-workerthread](../../packages/workflow/workflow-workerthread) (a `node:worker_threads` engine — one worker per run, the script's vm context inside it); the model-facing consumer is [dsh-tool-workflow](../../packages/workflow/tool-workflow). The proposal and rationale: [the dynamic-workflows Agent Note](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md).

View File

@@ -0,0 +1,132 @@
# 工作流
[English](workflow.md) | 中文
工作流 seam一个 agent智能体运行由模型编写的编排脚本SCRIPT扇出 subagent。与 [subagent](subagent.md) 一样,它是**一项可选能力**,不属于 agent loop智能体循环主干因此其词汇定义在此处而非 [core.md](core.md)。与 subagent 注册表不同,它采用 bash 形态:每个上下文只有一个引擎实现提供 `ctx.workflows`;没有命名提供方注册表(第二个引擎是插件替换,而非共存)。
接口:[dsh-workflow](../../packages/workflow/workflow)`ctx.workflows` + 下文词汇)。实现是 [dsh-workflow-workerthread](../../packages/workflow/workflow-workerthread)(一个 `node:worker_threads` 引擎——每个 run 一个 worker脚本的 vm 上下文位于其中);面向模型的消费方是 [dsh-tool-workflow](../../packages/workflow/tool-workflow)。提案与设计理由见 [dynamic-workflows Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md)。
源码:[`packages/workflow/workflow/src/types.ts`](../../packages/workflow/workflow/src/types.ts)
## 启动请求
调用方启动 run 时提出的请求。普通工作流工具根据模型的 `{ script, meta, args }` 调用与发起调用的 agent 构建它;专用消费方还可以为该 run 选择一个引擎级 `subagentProvider` 并调低 `maxTotalAgents`,但脚本无法观察或替换这两项策略。`meta``args` 是普通 JSON 数据(引擎会对 `meta` 做形状校验,并在任何内容运行前大声拒绝——绝不会通过求值脚本文本来获取它)。`parent` 是必填字段——脚本生成的每个子 agent 都归属于它cwd、谱系与深度通过 [subagent seam](subagent.md) 流转)。
```ts type-equiv
/**
* What a caller asks for when starting a workflow run. `meta` and `args` are
* plain JSON DATA by the seam contract (the tool builds both from the model's
* schema-validated call; the engine validates `meta`'s shape and rejects loud
* before anything runs) — an engine never evaluates script text to obtain
* them. `parent` is REQUIRED — every `agent()` the script spawns is
* attributed to it (cwd, lineage, depth flow through the subagent seam).
*/
interface WorkflowStartRequest {
/** The plain-JS script body (top-level await allowed; ends with `return <json-value>`). */
script: string
/** The workflow's identity block, as plain JSON data (shape-validated by the engine). */
meta: WorkflowMeta
/** Optional input exposed verbatim to the script as the `args` global. */
args?: unknown
/**
* Optional engine-wide child-provider override for this run. The workflow
* script cannot observe or replace it; omission uses the engine's configured
* provider.
*/
subagentProvider?: string
/**
* Optional per-run total-child ceiling. Implementations reject values above
* their deployment ceiling before publishing the run.
*/
maxTotalAgents?: number
/** The agent on whose behalf the run executes (parent of every child). */
parent: Agent
/** Cancels the run when aborted (the tool's `exec.signal`). */
signal?: AbortSignal
}
```
## 工作流的身份标识:`WorkflowMeta`
作为数据附在启动请求上的身份块(工具的 `meta` 参数;字段词汇与 Claude Code 动态工作流的 meta 块一致)。`phases` 仅用于进度展示:`phase()` 调用与标题匹配,供观察者使用;不暗示任何执行结构。
```ts type-equiv
/**
* The script's identity block, provided as plain JSON data alongside the
* script body (the model-facing tool carries it as its `meta` parameter) and
* validated by the engine before the body runs. `name`/`description` are
* required; the rest is optional annotation. The field vocabulary matches the
* Claude Code dynamic-workflows meta block.
*/
interface WorkflowMeta {
/** Short kebab-case workflow name (display + persistence key). */
name: string
/** One-line description of what the workflow does. */
description: string
/** Optional guidance on when this workflow applies (shown in listings). */
whenToUse?: string
/** Optional phase declarations matched by `phase()` calls. */
phases?: WorkflowPhase[]
}
```
## 终态结果:`WorkflowResult`
一次运行的结果,由 `WorkflowRun.result` resolve。`value` 是脚本的物化返回值——纯宿主域 JSON 数据(脚本无返回值时为 `null`)——仅在 `completed` 时有意义。`stopReason` 是封闭联合类型(引擎所有;消费方可穷举):`completed` | `cancelled` | `error`。非 `completed` 的原因在 `error` 中携带失败信息,消费方将其映射为 `isError` 工具结果,而非把部分输出当作成功上报。
```ts type-equiv
/**
* The outcome of one run, resolved by {@link WorkflowRun.result}. `value` is
* the script's materialized return value (plain host-realm JSON data; `null`
* when the script returned `undefined`) — meaningful only for `completed`.
* A non-`completed` reason carries the failure in `error`; the consumer maps
* it to an `isError` tool result rather than reporting partial output.
*/
interface WorkflowResult {
/** The script's return value (host JSON data; `null` for no return). */
value: unknown
/** Why the run settled. */
stopReason: WorkflowStopReason
/** The failure message (present iff `stopReason` is not `completed`). */
error?: string
/**
* How many `agent()` calls the run accepted over its whole lifetime. On a
* graceful settlement this is the script-side count (calls still queued for
* a concurrency slot included); on a termination path (grace force-settle,
* worker death) it degrades to the host-observed count — calls queued
* inside a terminated script are unknowable then.
*/
agentsStarted: number
}
```
## 活跃运行:`WorkflowRun`
脚本执行期间消费方持有的句柄。消费方 await `result`,可中途 `cancel`,且必须在每条路径上 `dispose`(资源释放)。`result` 不会 reject脚本失败以 `stopReason: 'error'` resolve一旦运行被取消即使脚本本身永不 settle它也会在引擎的有界宽限期内 settle引擎强制以 `cancelled` settleworker-thread 引擎随后终止脚本的 worker因此消费方 await `result` 不会在取消后卡死。`dispose()` = cancel + 有界 settle + 等待子 agent 停稳;它不会因脚本卡死而挂起。
```ts type-equiv
/**
* Holder-owned live workflow. `result` never rejects and settles within the
* engine's cancellation grace; failures resolve through `stopReason`. Consumers
* may cancel and must call idempotent `dispose()` on every path to await bounded
* script settlement and child quiescence.
*/
interface WorkflowRun {
readonly id: WorkflowRunId
/** The validated meta block (available before the body runs). */
readonly meta: WorkflowMeta
readonly result: Promise<WorkflowResult>
/** Cancel the run: children abort, pending hooks reject, the script dies at its next await (or is force-settled at the grace). */
cancel(reason?: string): void
/** Cancel + bounded-grace settle; safe to call on every path (idempotent). */
dispose(): Promise<void>
}
```
## 失败纪律:`WorkflowError.fatal`
脚本内部的钩子误用:错误参数、未知或延迟的 `agent()` 选项、超出[结构化输出子集](../../packages/core/tools/README.md)的 schema、触发的上限、seam 启动失败、取消,都会抛出 `fatal: true` 的 `WorkflowError`。`parallel()`/`pipeline()` 组合器对 fatal 错误直接重新抛出,而非将该项映射为 `null`:一个拼写错误的选项必须让脚本大声失败,绝不能消融为看似普通子 agent 失败的结果。逐项的 `null` 保留给子运行失败(非 `completed` 的 stop reason和阶段内的普通脚本错误。
## 事件
`workflow/*` 事件(`workflow/start`、`workflow/phase`、`workflow/log`、`workflow/agent-start`、`workflow/agent-end`、`workflow/end`,见[事件目录](../cordis-catalog/events.md))是**仅供观察**的 emit携带数据快照每个 payload 以 `WorkflowRunInfo`id + meta开头而非活跃的 `WorkflowRun`,因此订阅者无法获得 `cancel`/`dispose``workflow/end` 刻意省略 result value观察结果的监听器不得收到调用方 result 的可变别名)。每次 emit 对每个监听器隔离:抛出异常的订阅者被记录日志但不传播,不会饿死在它之后注册的监听器;每个监听器收到自己的 payload 克隆,因此修改它既不会损坏引擎也不会影响其他监听器。这种隔离方式与 `subagent/start`/`subagent/end` 一致。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
defensive-patterns.md: c69094db461048f5dbca5f8bdd1fb5581b08a962
defensive-patterns.zh.md: eb57f035ad0bd67e62e285d451502d41e4efc2bc

View File

@@ -1,5 +1,7 @@
# Defensive patterns
English | [中文](defensive-patterns.zh.md)
Hard-won bug-class rules: each pattern below is a class of defect that actually shipped or nearly shipped here, stated as the rule that prevents its recurrence. Read this before writing lifecycle, concurrency, subprocess, or teardown code. Test-tier counterparts (real entry path, world-verification, resource ownership) are in [testing.md](testing.md).
## Report orthogonal outcomes independently
@@ -12,7 +14,7 @@ When an interface documents two valid ways to signal something — an adapter ma
## Async state is not synchronous state
`agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-send result: several queued sends run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items. The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never `running`), the wait hangs — handle the "nothing to wait for" branch explicitly.
`agent.followup()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-follow-up result: several queued follow-ups run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items. The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never `running`), the wait hangs — handle the "nothing to wait for" branch explicitly.
## Dispose must reach quiescence, not just request it

View File

@@ -0,0 +1,29 @@
# 防御性模式
[English](defensive-patterns.md) | 中文
来之不易的缺陷类别规则下面每条模式都是本项目实际发布或差点发布的一类缺陷以防止其复发的规则形式陈述。在编写生命周期、并发、子进程或清理代码之前请先阅读本文。测试层面的对应规则真实入口路径、world 验证、资源归属)见 [testing.md](testing.md)。
## 正交结果独立上报
一个结果可以同时具有多重性质:进程可能既超时又以 exit 0 退出,因为它捕获了信号。每个独立事实(`timedOut``signal``exitCode`)都应独立暴露;切勿将某个 flag 的上报嵌套在另一个 flag 的分支内,否则调用方会把一次被截断的运行误读为正常成功。
## 跨 seam 契约两侧都要遵守
当一个接口文档记录了两种合法的信号方式时——例如适配器可以通过从 `stream()` 抛出异常来报告失败,也可以通过以 `finish {kind:'error'|'aborted'}` 分片结束流来报告——消费方必须同时处理两种路径,而不是只处理第一个实现恰好使用的那种。依赖库的适配器可能无法在流中途抛出异常,只能走带内路径;如果 agent loop智能体循环只捕获抛出的异常就会把提供方的 401 错误变成一个正常完成的轮次。请在类型定义处记录契约;通过真实消费方测试每个分支。
## 异步状态不是同步状态
`agent.followup()` 不会在返回前翻转状态;后台任务的完成与轮次边界存在竞争;`reader.close()` 在 EOF 和 dispose资源释放两种情况下都会触发。切勿基于一个刚刚请求的状态来控制流程——应以实际触发的事件/promise`agent/status``task.done`)驱动生命周期,并观察状态转换(先看到 `running` 再看到 `idle`),而不是把状态当作逐次 `followup()` 的结果:多次排队的 `followup()` 会在同一个 `running` 区间内连续运行多个轮次而取消或资源释放可能丢弃尚未启动的项。这条守则是双向的如果等待的转换永远不会发生EOF 时没有提交过任何工作 → 永远不会进入 `running`),等待就会挂起——请显式处理「无需等待」的分支。
## Dispose 必须达到完全停稳,而不仅仅是请求停止
一个清理流程如果发出 kill/abort 后就返回、而不等待工作实际停止,就会留下孤儿进程。请让清理逻辑异步化并 await 子进程退出kill → await `done`),并在 kill 之前关闭监听器/通知注册表,使迟到的完成事件保持静默。测试应证明 dispose 确实等待了(`await fiber.dispose()` 之后 pid 已不存在),而不仅仅是进程最终会死。
## 在边界处包容回调异常
用户提供的监听器如果抛出异常,不得导致它所在的 promise 被 reject也不得饿死排在它后面的监听器。请用 try/catch 包裹分发循环并记录日志;一个行为不当的订阅者绝不能破坏核心生命周期。
## 绝不将环境变量或可预测路径暴露给不可信输出
spawn 的命令应获得一份经过清洗的 env去除 `*KEY*`/`*SECRET*`/`*TOKEN*`),使 harness 凭证无法泄漏到输出、`env` 或溢出文件中。临时/溢出文件应使用私有0700目录、随机文件名和排他的仅所有者可访问打开方式`'wx'``0o600`)——可预测的全局可读路径会招致符号链接竞争和信息泄露。

View File

@@ -8,22 +8,24 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:353`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`tui`](../packages/ui/tui) |
| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:217`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:179`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:188`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:365`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) |
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:315`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy) |
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:246`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:262`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:207`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:276`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:330`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry), [`plan-mode`](../packages/plan/plan-mode) |
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:291`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:230`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:197`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) |
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:303`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:341`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode) |
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:352`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tool-goal`](../packages/goal/tool-goal) |
| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:350`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:285`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:294`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:498`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) |
| `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:326`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent) |
| `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:340`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent) |
| `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:316`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:448`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy) |
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:379`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:395`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:409`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:463`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry), [`plan-mode`](../packages/plan/plan-mode) |
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:424`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:363`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:303`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) |
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:436`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:474`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode) |
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:485`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tool-goal`](../packages/goal/tool-goal) |
| `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:30`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/ui/acp) |
| `commands/change` | `emit` | [`packages/ui/commands/src/index.ts:103`](../packages/ui/commands/src/index.ts) | [`commands`](../packages/ui/commands) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`tui`](../packages/ui/tui) |
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:62`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |

6
docs/glossary.i18n.yaml Normal file
View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
glossary.md: 0270a2d0dba558483e8e458a932a27b0151f2c93
glossary.zh.md: ed3009a054815f1c7165fc322e44cc9521527643

View File

@@ -1,5 +1,7 @@
# Glossary
English | [中文](glossary.zh.md)
Domain vocabulary for the DeepSeek Harness SDK uses one canonical term per concept. Terms link to their entries with standard Markdown anchors; implementation detail stays in package READMEs and Agent Notes.
FIXME(glossary-completeness): Expand this glossary before the first release so it covers the SDK's other core and capability subsystems, not only agent scope.
@@ -19,7 +21,7 @@ FIXME(glossary-completeness): Expand this glossary before the first release so i
## goal
- **goal** — one durable completion objective attached to an existing session, with a revisioned `active` / `paused` / `blocked` / `complete` phase and a goal-round cap; `blocked` retains a policy code and explanation. A goal is state, not a scheduler or a separate conversation; the session log remains its source of truth.
- **goal round** — one continuation cycle admitted for the current goal. The same-session driver materializes a goal round as one goal-sourced [turn](#turn), which can contain multiple steps; unrelated human turns in the same session do not consume the goal-round cap. <a id="goal-round"></a>
- **goal round** — one continuation cycle admitted for the current goal. The same-session driver materializes a goal round as one goal-sourced [turn](#turn), which can contain zero or more steps; unrelated human turns in the same session do not consume the goal-round cap. <a id="goal-round"></a>
- **goal activation** — process-local permission for a continuation consumer to admit another goal round. Activation is either `armed` or `disarmed`; it is deliberately absent from durable replay, so resume and fork require a later human-authorized resume mutation through `/goal` or the model tool before automatic work.
## human command
@@ -31,7 +33,7 @@ FIXME(glossary-completeness): Expand this glossary before the first release so i
## loop hierarchy
- **turn** — one drain of admitted input in a session, ending after the model and its tools stop or a terminal policy intervenes. <a id="turn"></a>
- **step** — one model request plus the tool executions caused by its response; a turn contains one or more steps. <a id="step"></a>
- **step** — one model request plus the tool executions caused by its response; a turn contains zero or more steps. <a id="step"></a>
- **round** — an outer policy iteration containing a turn, such as a [goal round](#goal-round) or one fresh-agent Ralph attempt. Round counters belong to that policy and do not count every turn in a session. <a id="round"></a>
## Ralph

43
docs/glossary.zh.md Normal file
View File

@@ -0,0 +1,43 @@
# 术语表
[English](glossary.md) | 中文
DeepSeek Harness SDK 的领域词汇为每个概念规定一个规范术语。各术语通过标准 Markdown 锚点链接到相应条目实现细节留在各包package的 README 与 Agent Noteagent 决策记录)中。
FIXME(glossary-completeness): 首次发布前扩充本术语表,使其覆盖 SDK 的其他核心与能力子系统,而非仅限于 agent scope。
## agent-scope
- **scope**:按 agent智能体划分的注册单位。一项贡献工具、提示词片段、变量、限制、监听器要么是*全局的*(对所有 agent 可见),要么是*有范围的*(归属于恰好一个 [scope key](#scope-key))。只有两层,扁平结构:有范围的注册不会向下继承给 subagent子树行为通过 [lineage](#lineage) 数据表达,从不通过 scope 结构。
- **scope key**scope 的不透明标识按对象同一性比较。harness 约定:一个活跃的 agent 就是其自身 scope 的 key。<a id="scope-key"></a>
- **agent 上下文(`agent.ctx`**agent 的有范围上下文;通过它进行的注册既是 scope 可见的,也是 scope 生命周期的(同一事实决定两者),其上的监听器参与该 agent 的 scope 过滤分发。注册表主体事件可以在各自的事件契约下保持故意不过滤。
- **scope carrier**scope 过滤分发所携带的 `thisArg`(由 `scopeTarget` 构建);其过滤器放行无标签监听器加上主体自身的监听器。*无主体*的 carrier没有 key只放行无标签监听器。
- **scoped dispatch**:规则是:关于某个 agent 活动的事件以该 agent 的 carrier 进行分发。关于注册表本身的事件(如「一个工具被添加了」)属于*注册表主体*事件,保持不过滤。
- **shadowing**:最具体者胜出的名称解析:一个有范围的工具/片段/变量仅在该 scope 内替换同名的全局对应项。这是按 agent 定制 persona 和按 agent 定制工具变体的机制。
- **restriction / scope-local 注册**restriction`tools.restrict`)为单个 scope 过滤全局工具表面(多个 restriction 取交集组合scope-local 注册在过滤之后合并。被过滤掉的全局工具既不出现在提示词中,也拒绝执行,与不存在的工具无法区分。
- **setup window**:创建者组装 agent 有范围世界的创建时隙(`CreateAgentOptions.setup`):在 scope 和 agent 对象已存在、但 agent 或会话尚未发布、`agent/session-start` 尚未触发、首次提示词尚未组装之前。setup 只做注册,从不驱动 agent。
- **lineage**:以数据形式携带的父子关系事实(`parentSession`、持久的 `delegationDepth`、运行时 `subagentDepth`);从不影响可见性。<a id="lineage"></a>
## 目标
- **目标**:附着在现有会话上的单个持久完成目标,带有按修订号演进的 `active` / `paused` / `blocked` / `complete` 阶段和 Goal Round 上限;`blocked` 保留策略代码与说明。目标是一种状态,不是调度器,也不是一段独立对话;会话日志仍是其真源。
- **Goal Round**:为当前目标接纳的一次续行周期。同会话驱动器将 Goal Round 具体化为一个来源为目标的[轮次](#turn),其中可包含零个或多个步骤;同一会话中无关的人类轮次不消耗 Goal Round 上限。<a id="goal-round"></a>
- **目标激活**:续行消费方接纳下一个 Goal Round 的进程本地权限。激活态为 `armed``disarmed`;它有意不参与持久回放,因此恢复和 fork 后,必须由人类随后通过 `/goal` 或模型工具授权恢复变更,自动工作才可开始。
## 人类命令
- **人类命令**:以斜杠开头的指令,由面向人类的适配器通过 `ctx.commands` 解释并执行,不会成为模型消息。它既不同于面向模型的工具,也不同于通过 `ctx.bash` 执行 shell 命令。
- **命令平面**:由 UI 适配器与命令插件拥有的发现、解析、分发、取消和结果渲染。除非处理器另行改变持久领域,否则命令输出属于 UI 状态。
- **目标命令**`/goal` 是由 `dsh-command-goal` 提供的人类命令;它直接观察或更改当前目标,而目标领域拥有每条持久且模型可见的记录。
## 循环层级
- **轮次**:会话中一次对已接纳输入的排空过程,在模型及其工具停止工作或终止策略介入后结束。<a id="turn"></a>
- **步骤**:一次模型请求,以及由模型响应引发的工具执行;一个轮次包含零个或多个步骤。<a id="step"></a>
- **Round**:承载一个轮次的外层策略迭代,例如一个 [Goal Round](#goal-round) 或一次使用全新 agent 的 Ralph 尝试。Round 计数器归该策略所有,并不统计会话中的每个轮次。<a id="round"></a>
## Ralph
- **Ralph 循环**:一次面向不可变目标的前台全新 agent 工作流运行。它是由工作流和 subagent 原语组合而成的面向模型的工具策略不是同会话目标、agent loop智能体循环模式、调度器或通用工作流脚本功能。<a id="ralph-loop"></a>
- **Ralph Round**[Ralph 循环](#ralph-loop)中的一个全新子会话。子会话不接收父会话或此前子会话的对话种子;共享工作区和一份有界的 [Ralph 交接](#ralph-handoff)承载跨 Round 的状态。<a id="ralph-round"></a>
- **Ralph 交接**:从一个仍需继续的 Ralph Round 传给下一个 Ralph Round 的规范化、有界结构化报告,包含状态、摘要、证据、后续步骤和阻塞说明。它补充共享工作区,而不取代工作区的权威地位。<a id="ralph-handoff"></a>

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
README.md: 430c499afbbfb786928276f6348cc0cedf14f94d
README.zh.md: 7ac7f4a2a8983c753def61df6f6d86a26405a3a0
README.md: 77d7b3210216c7c12d7d06b1ed16396d02ef1d16
README.zh.md: de15fc3b5f30c1280ce6b38c1afd2475be7f9671

View File

@@ -40,7 +40,7 @@ The gate's limit, stated plainly: **a green gate means the pair was confirmed co
**Excluded** (never paired, and the gate rejects a `.zh.md` or `.i18n.yaml` for them):
- `docs/cordis-catalog/`, `docs/tool-catalog/`, `docs/config-catalog.md`, `docs/persistence-catalog.md`, and `docs/module-graph.md` — generated files; their generators emit English only today, so a hand-written translation would go stale on every regeneration. The planned follow-up is to teach the generators to emit Chinese alongside English, at which point these leave the exclusion list.
- `docs/cordis-catalog/`, `docs/tool-catalog/`, `docs/config-catalog.md`, `docs/persistence-catalog.md`, `docs/module-graph.md`, `docs/agent-lifecycle.md`, `docs/capability-seams.md`, `docs/event-producer-consumer.md`, `docs/graph-atlas.md`, and `docs/tool-execution-pipeline.md` — generated files; their generators emit English only today, so a hand-written translation would go stale on every regeneration. The planned follow-up is to teach the generators to emit Chinese alongside English, at which point these leave the exclusion list.
- `docs/AGENTS.md` and `.agents/notes/**/AGENTS.md` — agent instructions, maintained in English only like the root `AGENTS.md`.
- `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction.
- [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior.

View File

@@ -40,7 +40,7 @@
**排除**(永不配对,门禁拒绝为它们建 `.zh.md` 或 `.i18n.yaml`
- `docs/cordis-catalog/`、`docs/tool-catalog/`、`docs/config-catalog.md`、`docs/persistence-catalog.md``docs/module-graph.md`:生成文件;生成器目前只输出英文,手写译文在每次重新生成时必然陈旧。计划中的后续工作是让生成器同时输出中文,届时这些文件移出排除清单。
- `docs/cordis-catalog/`、`docs/tool-catalog/`、`docs/config-catalog.md`、`docs/persistence-catalog.md``docs/module-graph.md`、`docs/agent-lifecycle.md`、`docs/capability-seams.md`、`docs/event-producer-consumer.md`、`docs/graph-atlas.md` 与 `docs/tool-execution-pipeline.md`:生成文件;生成器目前只输出英文,手写译文在每次重新生成时必然陈旧。计划中的后续工作是让生成器同时输出中文,届时这些文件移出排除清单。
- `docs/AGENTS.md` 与 `.agents/notes/**/AGENTS.md`agent 指令,与根 `AGENTS.md` 一样只以英文维护。
- `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md):二者本身即为中英对照文档。
- [translation-prompt.md](translation-prompt.md):自动翻译流水线的提示词模板;正文逐字进入模型请求,配对翻译会改变流水线行为。

View File

@@ -28,9 +28,9 @@
**dispose资源释放必须等待所有任务完全停稳不能仅下发终止指令就返回**:如果清理过程只发出终止或中断信号,却不等任务停止就返回,就会留下孤儿进程。清理应采用异步方式,等待所有子任务彻底退出(先发出终止信号,再等待退出);发出信号前应先关闭监听器与通知注册表,使延迟到达的完成事件不再触发通知。测试要证明 dispose 的确等到清理完成:执行完 `await fiber.dispose()` 后进程 PID 立即消失,不能只检查进程最终会自行消亡。
> **Async state is not synchronous state** — `agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-send result: several queued sends run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items.
> **Async state is not synchronous state** — `agent.followup()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-follow-up result: several queued follow-ups run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items.
**异步状态不等同于同步瞬时状态**:调用 `agent.send()` 不会在返回前同步更新状态;后台任务的完成时间与轮次边界存在竞态;`reader.close()` 既会在读到文件末尾时触发,也会在资源释放时触发。切勿把刚刚发起的状态变更当成已经生效,据此控制流程;生命周期逻辑应以实际触发的事件和已完成的 promise`agent/status``task.done`)为准,并观察完整的状态变化(先 `running`,再 `idle`),不要把状态当作逐次 `send()` 的结果:多次排队的 `send()` 会作为连续轮次运行,但可能共用一个 `running` 区间;取消或资源释放还可能丢弃尚未启动的队列项。
**异步状态不等同于同步瞬时状态**:调用 `agent.followup()` 不会在返回前同步更新状态;后台任务的完成时间与轮次边界存在竞态;`reader.close()` 既会在读到文件末尾时触发,也会在资源释放时触发。切勿把刚刚发起的状态变更当成已经生效,据此控制流程;生命周期逻辑应以实际触发的事件和已完成的 promise`agent/status``task.done`)为准,并观察完整的状态变化(先 `running`,再 `idle`),不要把状态当作逐次 `followup()` 的结果:多次排队的 `followup()` 会作为连续轮次运行,但可能共用一个 `running` 区间;取消或资源释放还可能丢弃尚未启动的队列项。
## ③ 测试政策清单

View File

@@ -52,6 +52,7 @@
| loader | loader | | | |
| manifest | manifest | manifest元数据清单 | | |
| monorepo | monorepo | | | |
| Round | Round | | 回合、目标回合、Ralph 回合 | 外层策略使用 Round 时,领域层级为 Session > Round > Turn轮次 > Step步骤Round 是可选的外层策略迭代并非每个会话轮次都具有的通用层级。Goal Round 与 Ralph Round 均保留英文。一个 Round 承载一个轮次,步骤隶属于该轮次;明确的零步骤轮次仍保持原义。 |
| schema | schema | | | |
| schema DSL | schema DSL | | | |
| seam | seam | | | 与 `extension point` 是不同概念;根据具体语境,可译为`服务边界``可替换点` |
@@ -103,10 +104,12 @@
| durability | 持久性 | | | |
| feature requirement | 功能依赖 | | | 功能或功能选项通过 `requires` 声明的关系 |
| enforcement frontier | 执行红线 | | 强制边界 | i18n 配对机制用语manifest `required` 清单所划的门禁生效范围与金标样例style-samples ⑦)一致 |
| ergonomics | 易用性 / 开发体验 | | 人体工学 | API 或面向模型的接口用「易用性」;工具链或开发者工作流用「开发体验」 |
| event | 事件 | | | |
| event log | 事件日志 | | | |
| event stream | 事件流 | | | |
| event-sourced | 事件溯源 | | | 沿用 DDD 社区通行译法 |
| Executive summary | 摘要 | | | 事故复盘标题用语 |
| executor | 执行器 | | | |
| expected output | 预期输出 | | 金标 | 指 snapshot 比较产物;翻译语料的人工校准样例不在此列 |
| extension | 扩展 | | | |
@@ -131,11 +134,14 @@
| mod | 模组 | | | |
| model provider | 模型提供方 | | | |
| module | 模块 | | | |
| non-escalation | 非升权 | | 非升级、不可升级 | 仅用于安全与权限语境,指主体不得获得超出既有授权的权限;普通升级不适用此行 |
| npm dependency | NPM 依赖 | | | `package.json` 中的包关系;`dependencies`、`devDependencies` 等字段保持原样 |
| opt-out ratio | opt-out 比例 | | 退出检查比例 | |
| orphan | 遗留 | | 孤儿、孤立 | 指英文源已不存在的 `.zh.md`(如「遗留译文」);进程语境按 OS 惯用语译「孤儿进程」 |
| orphan branch | 孤立分支 | | 孤儿分支 | 沿用 git 官方中文翻译 |
| package | 包 | 包package | | 指 npm 包(`@deepseek-ai/dsh-*``package.json` 等代码标识保持原样 |
| pairing | 配对 | | | |
| parent-subset grants | 父级子集授权 | | 父集合授权 | 指授权范围仅限于父级所持授权的子集 |
| peer dependency | 对等依赖 | 对等依赖peer dependency | | |
| permission | 权限 | | | |
| persistence | 持久化 | | | |
@@ -145,12 +151,14 @@
| provider | 提供方 | | | |
| provider-neutral | 提供方无关 | | | |
| quality gate | 质量门禁 | | | |
| quiescence | 完全停稳 | | 静默、静止状态 | 指生命周期工作全部结算后的状态 |
| reasoning | 推理 | 推理reasoning | | 需要和 `inference` 区分时保留英文括注 |
| reasoning_content | 思考内容 | | | |
| registry | 注册表 | | | |
| replay | 回放 | | | |
| resume | 恢复 | | | |
| runtime | 运行时 | | | |
| same-world subprocess | 与宿主共享文件系统和内核的子进程 | | 同世界子进程 | |
| sandbox | 沙箱 | | | |
| service | 服务 | | | |
| serving surface | 对外服务接口 | | | |
@@ -167,6 +175,7 @@
| stream | 流 | | | |
| streaming | 流式输出 | | | |
| structural signature | 结构签名 | | | i18n 配对机制用语:门禁比对两侧文件时提取的有序结构序列(标题层级、代码块、列表等) |
| Summary | 概述 | | | 事故复盘标题用语 |
| system prompt | 系统提示词 | | | |
| taxonomy | 分类体系 | | | |
| token usage | token 用量 | | | |

View File

@@ -24,14 +24,13 @@ export type SurfaceEventType =
| 'user/message'
| 'assistant/message'
| 'tool/result'
| 'context/message'
| 'steering/message'
/**
* How a session event entered the ordered surface. Only valid on
* {@link SurfaceEventType} events.
*
* - `'append'`: added to the tail — normal path for user/assistant/tool/context
* - `'append'`: added to the tail — normal path for user/assistant/tool/steering
* messages.
* - `{ op: 'replace', start, end }`: replaces surface nodes from `start`
* (inclusive) through `end` (inclusive) with this node. Both must exist as
@@ -51,7 +50,7 @@ export type SurfaceOp =
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`, `context/message`, `steering/message`).
* `assistant/message`, `tool/result`, `steering/message`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
@@ -79,7 +78,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}[T]
```
Sources: [`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:330`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:360`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:392`](../packages/core/session/src/types.ts)
Sources: [`packages/core/session/src/types.ts:325`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:338`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:367`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:399`](../packages/core/session/src/types.ts)
## Events
@@ -151,7 +150,7 @@ Source: [`packages/ui/user-approval/src/index.ts:67`](../packages/ui/user-approv
Types: [StreamChunk](core-data-structures/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/types.ts)
#### `assistant/message` — surface
@@ -167,7 +166,7 @@ Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/
Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:270`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:278`](../packages/core/session/src/types.ts)
### `compact/*`
@@ -221,33 +220,6 @@ Types: [ContentBlock](core-data-structures/core.md)
Source: [`packages/compact/compact/src/types.ts:22`](../packages/compact/compact/src/types.ts)
### `context/*`
#### `context/message` — surface
```ts persistence-catalog
/**
* In-session context injection (file-change notices, subdir AGENTS.md,
* skill content, cron notifications, …). Rendered into the derived history
* as a synthetic user-role message carrying `content` verbatim — NOT a
* user prompt. `meta` is durable JSON state omitted from the model
* projection; it is also the intended channel for any future framing
* directive (a producer declares the frame, a dedicated renderer applies it —
* see the deferred note in
* ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md),
* so the surface keeps projecting `content` verbatim rather than wrapping it.
*/
'context/message': {
content: ContentBlock[]
source: MessageSource
meta?: JsonValue
}
```
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:257`](../packages/core/session/src/types.ts)
### `hook/*`
#### `hook/invoked` — log-only
@@ -367,7 +339,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:41`](../packages/plan/plan-mode/s
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:245`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:269`](../packages/core/session/src/types.ts)
### `request/*`
@@ -381,7 +353,7 @@ Source: [`packages/core/session/src/types.ts:245`](../packages/core/session/src/
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
Source: [`packages/core/session/src/types.ts:305`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:313`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -437,7 +409,7 @@ Source: [`packages/session-title/session-title-llm/src/index.ts:44`](../packages
'steering/message': PromptMessageData & { turn: number }
```
Source: [`packages/core/session/src/types.ts:298`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:306`](../packages/core/session/src/types.ts)
### `step/*`
@@ -448,7 +420,7 @@ Source: [`packages/core/session/src/types.ts:298`](../packages/core/session/src/
'step/end': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:254`](../packages/core/session/src/types.ts)
#### `step/start` — log-only
@@ -457,7 +429,7 @@ Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/
'step/start': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts)
### `todo/*`
@@ -470,7 +442,7 @@ Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/
Types: [TodoItem](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:300`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:308`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -487,7 +459,7 @@ Source: [`packages/core/session/src/types.ts:300`](../packages/core/session/src/
Types: [CallId](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:276`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:284`](../packages/core/session/src/types.ts)
#### `tool/code-dispatch` — log-only
@@ -541,7 +513,7 @@ Source: [`packages/core/tools/src/code-mode.ts:34`](../packages/core/tools/src/c
Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:288`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:296`](../packages/core/session/src/types.ts)
### `turn/*`
@@ -559,7 +531,7 @@ Source: [`packages/core/session/src/types.ts:288`](../packages/core/session/src/
Types: [TurnEndReason](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:250`](../packages/core/session/src/types.ts)
#### `turn/start` — log-only
@@ -575,15 +547,23 @@ Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/
Types: [TurnTrigger](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:227`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:243`](../packages/core/session/src/types.ts)
### `user/*`
#### `user/message` — surface
```ts persistence-catalog
/** A user-visible prompt (the queued message claimed for this turn). */
/**
* A user-role message on the model-visible surface: a direct human prompt
* (the queued message claimed for this turn), a synthetic `agent.inject()`
* context (file-change notices, subdir AGENTS.md, skill content, cron
* notifications, …), or an admitted goal continuation round. All three
* project their `content` verbatim; `source` (with a non-`user` kind marking
* injected context) is the only channel that tells them apart. An idle
* injection wraps this event in a one-shot turn so the log stays turn-enclosed.
*/
'user/message': PromptMessageData
```
Source: [`packages/core/session/src/types.ts:240`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:264`](../packages/core/session/src/types.ts)

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
0001-acp-default-export-drops-inject.md: ab3efc880cb5290dc149b6bacb276ccf581968c1
0001-acp-default-export-drops-inject.zh.md: 763b2e6230cf42dcd6985df3d7e2da8766176fd5

View File

@@ -1,5 +1,7 @@
# Post-mortem 0001: ACP server crashed on connect — `export default` dropped the plugin's `inject`
English | [中文](0001-acp-default-export-drops-inject.zh.md)
Status: resolved (fix in PR #41 `feat/acp-2-bridge`)
## Executive summary

View File

@@ -0,0 +1,113 @@
# 事故复盘postmortem 0001ACPAgent Client Protocol服务器在连接时崩溃——`export default` 丢弃了插件的 `inject`
[English](0001-acp-default-export-drops-inject.md) | 中文
Status: resolved (fix in PRPull Request #41 `feat/acp-2-bridge`)
## 摘要
两个集成错误在单元测试全覆盖的情况下仍然导致 ACP 崩溃:一个 default export 使 Loader 丢弃了 `inject`,一个经 traceable 代理的可选服务查找在 shadow 边界上失败。手动挂载的测试绕过了这两条路径。修复方案增加了无需 API key 的真实 Loader 覆盖率并为插件导出和可选服务访问制定了包package级规则。
## 概述
ACP 服务器(`examples/acp-agent``@deepseek-ai/dsh-acp`在真实编辑器Zed连接的瞬间崩溃第一个 `session/new` 请求返回 `Internal error: cannot get property "agents" without inject``session/load``sessionPersistence` 返回同样的错误。尽管有 178 个绿色单元测试和 100% 行覆盖率bridge 在生产环境中完全无法工作。两个独立的 bug 隐藏在同一个错误字符串背后,测试套件之所以两个都没捕获,原因也相同:所有测试都通过一条不会触及插件真实加载方式和服务真实解析方式的路径来挂载插件。
## 影响
ACP 服务器无法创建或加载任何一个会话——而这正是编辑器最先调用的两个 RPC。任何将 agent智能体接入 Zed 的人都会立即遭遇硬性失败。无数据丢失(崩溃前没有任何内容被持久化);代价完全是「功能不可用」加上两次定位原因的调试时间。
## 时间线
- bridgeRFC 010落地时附带完整的单元测试套件codec、内存传输、基于属性的协议形状测试、失败路径、HMR热模块替换、一个需要 key 的真实 API e2e 测试,以及一个无需 key 的 stdout 纯净性 e2e 测试。全部绿色100% 覆盖率。
- 真实 Zed 会话在 `session/new` 上立即失败,报错 `cannot get property "agents" without inject`
- 调查最初追踪了一个 Cordis「traceable/shadow」理论看似合理且该机制确实存在——见 Bug #2),随后在 vendor 的 `reflect.ts` 中对实际 fiber 遍历做了插桩并运行了真实子进程。trace 显示 throw 发生在 `apply()` 第 179 行、*插件加载时*,位于 ROOT fiber 且没有 shadow——推翻了 shadow 理论对 `session/new` 的解释。
- 找到根因 #1:一行多余的 `export default apply`。删除后 `session/new` 修复。
- 删除后暴露了 Bug #2`session/load` 仍然在 `sessionPersistence` 上抛错——这是一个真正不同的机制shadow 遍历),通过隔离修复并重新运行真实子进程得到确认。
## 根因 #1——`export default apply` 丢弃了插件的 `inject`(导致 `session/new` 崩溃)
`packages/ui/acp/src/index.ts` 是一个*命名空间插件*:它将 `name``inject``Config``apply` 作为独立的命名导出——与仓库中其他所有插件(`invariants``llm-deepseek``tool-bash``tui` 等)形状相同。但它*还*多了一行其他插件都没有的代码:
```ts ignore-check
export const name = 'acp'
export const inject = ['agents', 'sessions', 'sessionPersistence']
export function apply(ctx: Context, config: AcpConfig): void { /* … */ }
// …
export default apply // ← the bug
```
当插件从 `cordis.yml` 加载时Cordis Loader 通过 `Loader.unwrapExports``vendor/loader/src/index.ts`)对导入的模块进行规范化:
```ts ignore-check
unwrapExports(exports: any) {
if (isNullable(exports)) return exports
exports = exports.default ?? exports // ← prefers `.default`
if (!exports.__esModule) return exports
return exports.default ?? exports
}
```
存在 default export 时,`exports.default ?? exports` 解析为**裸 `apply` 函数**。裸函数没有 `inject`、没有 `name`、没有 `Config` 属性——这些作为*兄弟*命名导出存在于模块命名空间上,而 unwrap 到 `.default` 把整个命名空间丢弃了。Loader 随后基于空的 `inject` 构建了插件的 fiber。
因此 `apply` 在一个**没有注入任何服务**的 fiber 中运行。第一行 `const agents = ctx.agents` 遍历 fiber 树ROOT → Include → Loader → ROOT在所有 fiber 的 store 中都找不到 `agents`,到达根 fiber`runtime === null`)后抛出 `cannot get property "agents" without inject`。崩溃发生在*加载时*,而非后续的请求处理器中——请求只是恰好触发了加载。
**修复:** 删除 `export default apply`。Loader 随后使用模块命名空间,正确识别 `inject`/`name`/`Config``apply` 在一个真正授予了声明服务的 fiber 中运行。
## 根因 #2——可选服务读取通过 traceable shadow 触发 inject 守卫(导致 `session/load` 崩溃)
修复 #1 后,`session/new` 正常工作,但 `session/load` 仍然抛出 `cannot get property "sessionPersistence" without inject`。这个问题*确实*是 Cordis 的 traceable/shadow 机制,值得精确理解。
`session/load` 调用 `agents.resume(...)`,后者委托给 `AgentLoop.resume()`,其中读取了 `this.ctx.sessionPersistence`。`AgentLoop` 的 `static inject` 故意不包含 `sessionPersistence`——注入它会导致非持久化的演示永远挂起,等待一个永远不会加载的后端。该服务由一个独立的兄弟插件/fiber 提供,以机会性方式读取。
Cordis 中的服务访问通过上下文代理(`vendor/cordis/src/reflect.ts`)进行。当通过从外部 fiber 获取的 *traceable 代理*调用服务方法时此处bridge fiber 调用 `ctx.agents.resume`,注册表返回 `this.factory`——即 `AgentLoop`——重新包装为绑定到调用方的新 traceable 代理),`createShadowMethod``vendor/cordis/src/utils.ts`)将 `this` 重新绑定到一个 *shadow* 对象,其 `ctx` 携带 `[symbols.shadow]` 指向 `AgentLoop` 自身的构造上下文。在 `resume` 内部,`this.ctx.sessionPersistence` 的解析从 shadow 的 fiber 开始遍历:
```ts ignore-check
// reflect.ts get handler
let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber // ← starts at AgentLoop's fiber
while (true) {
const impl = fiber.store?.[prop]
if (impl) return getTraceable(ctx, impl.value)
if (prop in fiber.inject) { /* inactive-context error */ }
if (!fiber.runtime) throw error // ← reached root, throw
if (fiber.parent[symbols.isolate][prop] !== key) throw error
fiber = fiber.parent.fiber // ← ancestor-only
}
```
遍历**仅向祖先方向**进行。`sessionPersistence` 既不在 `AgentLoop` 的 fiber store 中(不在其 `static inject` 中),也不在通往 root 的任何祖先上(它位于一个*兄弟*分支),因此遍历到达根 fiber 后抛错。
为什么内存中的 `AgentLoop` 恢复测试没有捕获这个问题?因为它们从测试代码直接调用 `ctx.agents.resume(...)`——*在任何插件 fiber 之外*。此时 `ctx.fiber.runtime` 为 `null`,代理处理器走了一条提前绕过的路径:
```ts ignore-check
if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false) // ← direct global-store lookup, no fiber walk
```
`ctx.reflect.get(name, false)` 是基于 isolate symbol 的全局服务 store 直接查找——完全忽略 fiber 拓扑,能找到服务。因此从顶层测试读取可以成功;而从真实插件 fiber 内部、经由 shadow 到达时则抛错。bridge 恰好是后者。
**修复:** 使用 `ctx.get('sessionPersistence')` 读取可选服务,该方法使用全局 isolate-keyed store 同时保留活跃状态检查。对于插件声明注入集中的服务,直接属性读取仍然适用。
## 为什么所有测试都没有捕获(真正的失败)
两个 bug 共享同一个流程缺口:**没有任何测试通过插件的真实加载路径或真实调用拓扑来驱动它。**
- 内存 harness 通过手动构建插件对象来挂载 bridge`ctx.plugin({ name, inject, apply })`。这手动提供了 `inject`,因此永远无法复现 Bug #1——`unwrapExports` 只被 *Loader* 调用,`ctx.plugin` 从不调用它。即使 `ctx.plugin(NamespaceImport)` 也无法捕获。
- 同一个 harness 将所有内容平铺挂载在一个根上下文上,因此从中触达的 `AgentLoop` 恢复要么运行在顶层(`!runtime` 绕过),要么通过一个 origin 仍然解析在 root 上的 shadow——掩盖了 Bug #2 的祖先遍历失败。
- 唯一的无 key e2e 发送 `initialize` 并检查 stdout 纯净性。`initialize` 从不触达 factory因此两个 bug 都安然通过。
- 唯一驱动 `session/new`/`session/load` 的测试需要 key 才能运行,因此 CI无 key跳过了它——而本地它之所以「通过」只是因为一个陈旧的已构建 `lib/`(包含旧代码)恰好满足了模块解析。
100% 行覆盖率始终满足。覆盖率证明代码行*被执行过*;它不能说明功能是否*按交付方式正常工作*。
## 新增的防护措施
- **删除 `export default apply`**`packages/ui/acp/src/index.ts`——Bug #1 的修复。
- **`AgentLoop.resume` 使用 `this.ctx.get('sessionPersistence')`**`packages/core/agent-loop/src/index.ts`——Bug #2 的修复,附注释说明 shadow 遍历陷阱。
- **无需 key 的 `session/new` e2e通过真实 stdio 运行**`examples/acp-agent/tests/acp.e2e.ts`):以子进程方式通过真实 Loader 启动示例,并断言 `session/new` 正常返回。无需 API key 即可在 Bug #1 上大声失败。已验证恢复 `export default apply` 时测试失败。
- **e2e spawn 中设置 `TSX_TSCONFIG_PATH`**:子进程从临时 cwd 运行tsx 无法通过向上搜索找到仓库根的 tsconfig `paths` 映射——因此 dsh-* 的 import 静默回退到已构建的 `lib/`。将 tsx 指向仓库 tsconfig 使解析不依赖 cwd确保测试运行的是*源码*而非可能陈旧的构建产物。
- **[docs/testing.md](../testing.md) 规则**:「测试真实入口路径」,行覆盖率不等于行为覆盖率——将这一教训编纂为所有未来插件的规则。
## 经验教训
- 命名空间插件与 default export 在 Cordis Loader 下互斥。选择命名空间形式(`name`/`inject`/`Config`/`apply`),不要添加 `export default`——`unwrapExports` 会丢弃命名空间。
- 对于插件机会性读取但未在 `static inject` 中声明的服务,使用 `ctx.get(name)`,绝不使用 `ctx.<name>`。属性代理通过仅向祖先方向的 fiber 遍历解析,经由外部 shadow 时会失败;`ctx.get(name)` 是拓扑无关的查找(且默认严格——非活跃后端读取为 `undefined`,而非在 teardown 过程中被交出)。
- 手动构建插件的测试无法验证插件的加载方式。至少一个测试必须端到端地驱动真实的 Loader/export 路径。当核心操作不调用模型时,该测试无需 API key——因此它属于 CI而非 key 门控之后。
- 相信 trace不要相信理论。优雅的 shadow 解释是真实的,但它是*第二个* bug*第一个*是一行导出错误,在数小时看似合理但实际错误的推理之后,一个 fiber 遍历的 `console.error` 在几分钟内就找到了它。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
0002-js-expression-disabled-filesystem-tools.md: 30ff9d920821a8d55c4bea5f120f1aeeca6634b3
0002-js-expression-disabled-filesystem-tools.zh.md: b103ec6de5d6d6406ba48ec34f6ebb479e472352

View File

@@ -1,5 +1,7 @@
# Post-mortem 0002: Filesystem snapshot tools were permanently disabled
English | [中文](0002-js-expression-disabled-filesystem-tools.zh.md)
Status: resolved
## Executive summary

View File

@@ -0,0 +1,47 @@
# 事故复盘postmortem 0002文件系统快照工具被永久禁用
[English](0002-js-expression-disabled-filesystem-tools.md) | 中文
Status: resolved
## 摘要
ACPAgent Client Protocol示例试图通过 `disabled: !!js ...` 有条件地启用文件系统插件,但 Cordis 仅在插件 `config` 内部对 JavaScript 表达式求值。原始的表达式对象为 truthy因此文件系统栈始终处于禁用状态。快照刷新随后将 `UNKNOWN_TOOL` 结果接受为新的预期输出。修复方案改用显式的文件系统 overlay并增加了静态配置守卫和快照结果守卫。
## 概述
默认的 ACP 组合有意只启用 bash因为其沙箱无法约束进程内的文件系统提供方。文件系统快照场景仍然需要 `read``write``edit`,因此这些插件被放在默认的 `cordis.yml` 中,并附带一个 `disabled` 表达式,意图仅在全权限启动和快照模式下启用它们。
Cordis Include 将每个 `!!js` 标量解析为一个表达式对象。Loader 递归地对插件的 `config` 进行插值,但直接消费 `disabled` 等入口元数据。因此每个文件系统入口看到的都是一个 truthy 对象,在所有模式下均保持禁用。
## 影响
七个文件系统场景和一个混合工作区编辑场景调用了注册表中不存在的工具。其结构化会话日志携带 `ToolNotFoundError`code 为 `UNKNOWN_TOOL`stdout 渲染出通用的失败工具卡片。快照套件通过了,因为结构化会话日志和 stdout 渲染出的通用失败工具卡片均与刷新后的 fixture测试前置数据匹配它证明的是回归的确定性回放而非文件系统行为的正确性。
实际运行的受限默认模式并未获得意外的文件系统访问权限。一个简单的插值修复反而会制造该风险:权限预设在运行时更新 bash 沙箱和审批状态,但无法挂载、卸载或约束文件系统栈。
## 时间线
- PRPull Request #261 整合了 ACP 组合并刷新了文件系统快照,同时引入了条件式文件系统入口。
- 所有单元测试、覆盖率、快照、文档、构建和 hygiene 检查均通过。
- 对刷新后的文件系统预期输出的评审发现了通用的失败卡片和结构化的 `UNKNOWN_TOOL` 结果。
- 一次真实的 Loader 启动确认:每个 `disabled` 值仍为表达式对象,每个文件系统 fiber 均未创建。
## 根因
实现时假设 `!!js` 适用于整个 Loader 入口。其实际边界更窄:`Entry._resolveConfig()` 仅对 `entry.options.config` 进行插值;`Entry.disabled` 直接测试 `entry.options.disabled`不经过插值。YAML 标签在语法上合法,因此加载过程不产生任何诊断信息。
快照框架将任何确定性的 transcript文本记录视为有效行为。Header pin 验证了组合后的工具 schema但文件系统场景共享来自默认组合的 pin因此未独立证明其所需工具已注册。刷新在任何语义断言拒绝缺失工具之前就已重写了预期的 stdout 和会话日志。
## 已添加的防护措施
- 文件系统场景启动 `fs.cordis.yml`:一个显式的固定全权限 overlay配有对应的回放配置和独立的 request-header 类。
- [`AGENTS.md`](../../AGENTS.md) 与 [Cordis 入门](../cordis-primer.md#loader-configuration)明确说明 `!!js` 仅在插件 `config` 内有效,条件式组合应使用 overlay。
- `verify-cordis-config` 解析仓库中的 Cordis YAML拒绝 Loader 入口元数据中的表达式节点(包括 include patch 和插入的入口)。
- `dsh-acp-snapshot` 在新鲜运行和已提交的会话 fixture 中拒绝结构化的 `UNKNOWN_TOOL` 结果,防止其被提交为预期输出。
## 教训
- 语法上被接受的配置值不一定在该位置被求值;应记录并验证插值边界。
- 快照刷新是 fixture 的生产过程,不是正确性审查。诸如已注册工具缺失这类语义上不可能的结果,需要独立于预期输出的断言。
- 权限控制只应描述其实际管辖的能力。组合时的文件系统访问无法安全地跟随运行时的 bash-only 预设。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: df0e2fcb8540aeed005153dbecc451d781ca5ff1
README.zh.md: 2ce6de475c705b02cd9dabfb2181929d81478e2c

View File

@@ -1,5 +1,7 @@
# Post-mortems
English | [中文](README.zh.md)
Incident write-ups: a bug reached a place it shouldn't have (a real user, a merged PR, a release), and the interesting part is *why our process let it through*, not just the one-line fix.
A post-mortem is NOT an [Agent Note](../../.agents/notes/README.md) (which records a deliberate design decision and its rejected alternatives, or proposes future work). It is a backward-looking record of a failure: what broke, the mechanism, why every safety net missed it, and the concrete guardrails added so the same class of bug fails loudly next time.

View File

@@ -0,0 +1,16 @@
# 事故复盘postmortem
[English](README.md) | 中文
事故复盘:一个 bug 到达了它不该到达的地方(真实用户、已合并的 PRPull Request、已发布的版本值得关注的是*为什么我们的流程放过了它*,而不仅仅是那一行修复。
事故复盘不是 [Agent Noteagent 决策记录)](../../.agents/notes/README.md)Agent Note 记录一个经过深思熟虑的设计决策及其被否决的替代方案,或提出未来工作)。它是一份回顾性的失败记录:什么坏了、机制是什么、为什么每道安全网都没拦住、以及加了哪些具体的防护措施使同类 bug 下次能被显式暴露。
当一个 bug 满足以下条件时,请撰写事故复盘:**隐蔽**(机制不显而易见,即使是细心的工程师也得费力重新推导)、**系统性**(逃逸的原因是测试/工具/约定的缺口,而非一次性的笔误)、**重新发现的代价高**它消耗了真实的调试时间且下次还会如此。请链接该事故复盘所推动建立的防护措施测试、AGENTS.md 规则、ADR
每篇事故复盘以一段**摘要**开头:一个简短段落,让忙碌的读者在三十秒内吸收要点——什么坏了、用直白的话说根因是什么、为什么逃逸了、持久的教训是什么——然后才是后续的详细「概述 / 时间线 / 根因 / 防护措施」各节。
| # | 标题 |
|---|---|
| [0001](0001-acp-default-export-drops-inject.md) | ACPAgent Client Protocol服务器在连接时崩溃`export default` 丢失了插件的 `inject` |
| [0002](0002-js-expression-disabled-filesystem-tools.md) | 文件系统快照工具被一个字面量 `!!js` 对象永久禁用 |

6
docs/testing.i18n.yaml Normal file
View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
testing.md: fd38fb7b20d76ef48c81c86badcf501f7c0dbd4e
testing.zh.md: 4584492350aefd5d72692093b08c0dcd4910a8af

View File

@@ -1,21 +1,23 @@
# Testing policy
English | [中文](testing.zh.md)
How this repo tests, tier by tier, and the rules that keep a green suite meaningful. Commands live in root [AGENTS.md](../AGENTS.md); linked Agent Notes carry the rationale.
## Tiers
- **Unit** (`pnpm run test`): vitest over `packages|examples/*/tests/**/*.spec.ts`, colocated with what they test. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`).
- **Unit** (`pnpm run test`): vitest over package and example specs under their `tests/**` directories plus repository script specs under `scripts/**/*.spec.ts`; tests stay with the code area they exercise. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`).
- **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped.
- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)).
- **Snapshot** (`pnpm run test:snapshot`): transport-specific keyless expected outputs cover external presentation. ACP suites boot the real example subprocess, replay a recorded session, and diff normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); the headless suite independently pins `stream-json` through its real one-shot subprocess. TUI completed journeys replay recorded primary/child JSONL through the real agent loop and tools before projecting ANSI into semantic terminal-state expected outputs; package-local snapshots retain transient renderer states, and a real PTY conversation covers the process boundary ([TUI snapshot Agent Note](../.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md)). Use `pnpm run test:snapshot:record` when a model transcript must change and `pnpm run test:snapshot:refresh` when committed replay input remains correct; review every JSONL and expected-output diff. System-prompt/tool-schema content is pinned by one ACP scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header Agent Note](../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
- **Snapshot** (`pnpm run test:snapshot`): keyless expected outputs cover external presentation. ACP boots the real example, replays a recorded session, and diffs normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); headless pins `stream-json` through its real one-shot process. TUI journeys replay primary/child JSONL through the real loop and tools, then project ANSI into semantic terminal-state outputs; package snapshots retain transient states and a real PTY covers the process boundary ([TUI snapshot Agent Note](../.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md)). Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every JSONL and expected-output diff. One ACP scenario (`text-turn`) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line ([pinned-header Agent Note](../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
## The with-key policy: inference is cheap here
We are DeepSeek — do not ration real-API tests. A no-key test proves plumbing; only a with-key run proves the agent works against a real model. Write many: file-writing prompts, multi-turn conversations, tool use, cancellation mid-stream. Highest-value are **smoke tests** that boot the real example, send one real prompt, and check the world — they catch the "green unit tests, broken product" class that mocks structurally cannot ([postmortem 0001](postmortem/0001-acp-default-export-drops-inject.md)). The self-skip exists only so secretless CI and keyless contributors aren't blocked; it is not a cost signal. Every example ships both a keyless smoke and a with-key smoke ([examples/AGENTS.md](../examples/AGENTS.md)).
We are DeepSeek — do not ration real-API tests. A no-key test proves plumbing; only a with-key run proves the agent works against a real model. Cover file-writing prompts, multi-turn conversations, tool use, and mid-stream cancellation. Highest-value are **smoke tests** that boot the real example, send one prompt, and check the world — they catch the "green unit tests, broken product" class that mocks cannot ([postmortem 0001](postmortem/0001-acp-default-export-drops-inject.md)). Self-skip keeps secretless CI and keyless contributors unblocked; it is not a cost signal. Every example ships keyless and with-key smokes ([examples/AGENTS.md](../examples/AGENTS.md)).
## Prefer the real implementation over a mock
Mock only the genuinely expensive or non-deterministic boundary (the LLM adapter, the network, the clock); keep everything downstream real. A hand-rolled stand-in proves the bridge moves bytes, not that the shipping tool behaves as asserted — the two drift while the test stays green. Example: bridge tool-call tests run the scripted mock MODEL but the real tool + real executor (`makeBridgeHarness({ withBash: true })` plugs `dsh-bash-local` + `dsh-tool-bash` and runs an actual `echo`).
Mock only the expensive or non-deterministic boundary (LLM adapter, network, clock); keep everything downstream real. A hand-rolled stand-in proves the bridge moves bytes, not that the shipping tool behaves as asserted. Bridge tool-call tests use the scripted mock model with the real tool and executor: `makeBridgeHarness({ withBash: true })` plugs in `dsh-bash-local` and `dsh-tool-bash`, then runs `echo`.
Recovery tests separate pre/post-chunk failures by step and prove failed chunks derive no message or tool side effect. Cover exhaustion, cancellation, policy composition, persistence, status, wire counts, transport-closing idle timeouts, and shipping Loader composition.

46
docs/testing.zh.md Normal file
View File

@@ -0,0 +1,46 @@
# 测试策略
[English](testing.md) | 中文
本文说明本仓库的分层测试方式,以及保持绿色测试套件有意义的规则。命令见根目录 [AGENTS.md](../AGENTS.md);相关 Agent Noteagent 决策记录)承载设计动机。
## 层级
- **单元测试**`pnpm run test`vitest 运行包package和示例各自的 `tests/**` 目录下的测试,以及匹配 `scripts/**/*.spec.ts` 的仓库脚本测试;测试文件与其所覆盖的代码区域放在一起。每个注册表都有一个 HMR热模块替换安全测试dispose资源释放贡献的 fiber断言清理完成。优先覆盖边界情况、错误路径、事件顺序、并发竞态以及永久性契约回归`packages/core/agent-loop/tests/contract-regressions.spec.ts`)。
- **覆盖率门禁**`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。
- **真实 API e2e**`pnpm run test:e2e`):带密钥测试调用真实提供方 API包括 DeepSeek 模型以及各提供方特有的冒烟测试;这些测试各自由自己的密钥控制(`EXA_API_KEY``PERPLEXITY_API_KEY` 等),缺少密钥时套件会自动跳过,使 keyless CI 保持绿色([真实 API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md))。
- **快照**`pnpm run test:snapshot`无密钥预期输出覆盖对外呈现。ACP 启动真实示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diff[ACP 快照 Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)headless 通过真实单次运行进程固定 `stream-json`。TUI 旅程通过真实循环与工具回放主会话与子会话 JSONL再将 ANSI 投影为语义化终端状态输出;包级快照保留瞬态状态,真实 PTY 覆盖进程边界([TUI 快照 Agent Note](../.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md))。当模型 transcript文本记录发生变化时使用 `pnpm run test:snapshot:record`,回放输入仍然有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(`text-turn`)固定完整的系统提示词与工具 schema 内容;其他 fixture测试前置数据将其 token 化,因此修改只会扰动一行([pinned-header Agent Note](../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
## 带密钥策略:推理在这里很便宜
我们是 DeepSeek不要吝惜真实 API 测试。无密钥测试只能证明底层通路;只有带密钥运行才能证明 agent智能体能对接真实模型正常工作。覆盖文件写入提示词、包含多个轮次的对话、工具使用和流中取消。价值最高的是**冒烟测试**:启动真实示例、发送一条提示词,并检查外部世界;它们能捕获「单元测试全绿、产品却坏了」这一类 mock 无法发现的问题([事故复盘 0001](postmortem/0001-acp-default-export-drops-inject.md))。自动跳过让无密钥 CI 和无密钥贡献者不受阻塞;它不是成本信号。每个示例都提供无密钥和带密钥冒烟测试([examples/AGENTS.md](../examples/AGENTS.md))。
## 优先使用真实实现而非 mock
只 mock 开销高或不确定的边界LLM大语言模型适配器、网络、时钟下游一切保持真实。手写替身只能证明桥接层在搬运字节不能证明交付的工具行为符合断言。桥接工具调用测试将脚本化 mock 模型与真实工具和执行器配合使用:`makeBridgeHarness({ withBash: true })` 接入 `dsh-bash-local``dsh-tool-bash`,然后运行 `echo`
恢复测试按步骤区分分片前与分片后的失败,并证明失败分片不会派生出消息或工具副作用。覆盖耗尽、取消、策略组合、持久化、状态、协议计数、会关闭传输的空闲超时,以及交付的 Loader 组合。
## 验证外部世界,而非自我报告
e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身输出做关键词探测会让作弊的 agent 通过。断言未修改的文件逐字节一致。e2e 测试自行管理资源:在测试中创建 harness`afterEach` 中 dispose即使失败/重试/超时也要释放);共享 fixture 放在普通的 `tests/harness.ts` 中,绝不放在另一个 `*.e2e.ts` 中(导入一个 spec 会重新注册其 `describe`,导致真实 API 调用重复执行)。
## 测试真实入口路径
- 产品可见的插件必须有一个非单元的真实组合测试。手动构建的 `ctx.plugin(...)` 套件不够:通过 Loader 和 app/process 启动仅用于测试的 `cordis.yml`,只 mock 外部/不确定边界,断言模型可见的请求/日志、持久状态或用户可见输出。不要把 opt-in 选项混入交付默认值。
- 一个守卫只有在回归真的能让它失败时才有效。对于没有 `inject` 的插件bundle/组合插件Loader 冒烟测试在导出形状损坏时仍然绿着——需要添加显式的 `expect('default' in mod).toBe(false)``unwrapExports` 往返断言,并证明它有效:引入回归、观察变红、回退。
- 「真实入口路径」指已发布的产物:包的 `bin` 所运行的是构建后的 `lib/bin.js`,并由普通 `node` 执行,从而暴露 tsx 会掩盖的失败(等待稳定时的竞态、模块解析、被吞掉的加载失败)。同样的规则适用于非 index 运行时入口worker-thread 的同级文件 `lib/worker.cjs`),也适用于多个 bundle 共享的单例模块(`packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts`)。保持构建产物冒烟测试绿色(`packages/ui/*/tests/built-bin.e2e.ts``packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`),并断言真正缺失的配置以非零状态退出。
## 测试解析:仅限源码
- 每个 vitest 配置都将 vite-tsconfig-paths 指向 `tsconfig.base.json`;工作区包的裸导入解析到 `src`[布局](development.md#typescript-project-layout)),绝不会经由包的 `exports` 解析到构建后的 `lib/`,因为其中的陈旧产物会加载第二份模块单例。构建产物只在显式指定时使用:以 `lib` 模式运行的子进程,以及下文的构建产物冒烟测试。
## 测试子进程启动模式
- CI 与已有构建产物的测试通道通过共享双模式启动器,从构建后的 `lib/` 运行每个示例或 Cordis 配置子进程。不要为这些子进程手写 `--import tsx`
- 不加载 Cordis 的协议与操作系统 fixture 直接通过 Node 运行使用可擦除语法的 `.ts` 文件,不经过 tsx 或根路径映射。
- 只有测试对象本身是源码路径解析时,才可以选择 `src`;在测试中写明这一契约。
## 何时需要快照测试
每项非平凡的模型可见或人类可见变更,都必须在同一 PR 中通过可运行示例所属的快照套件添加或更新无密钥场景。包测试、e2e 断言、mock 与仅测试组合、PR 理由都不能取代组装后的 transcript必要时应扩展 harness。ACP 接口使用 `examples/<name>/tests/snapshots/`,即基于 [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) 套件工厂的场景表(`examples/acp-agent` 为主套件);`examples/headless-agent` 拥有 `stream-json` 快照与回放 fixture。已完成的交互式终端旅程使用 `examples/tui-agent/tests/snapshots/` 下由 JSONL 驱动的场景瞬态呈现使用包内语义矩阵输入、Loader 选择或终端清理发生变化时还要添加 PTY 用例。新的能力 seam、生命周期形态或 transcript 呈现接口在计划阶段就要列出每个覆盖层级,并在实现前验证 harness 能够表达它们。

View File

@@ -23,12 +23,12 @@ This table connects model-visible tool names to the plugin package and service s
| `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after successful file operations`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin. |
| `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.bash`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. |
| `@deepseek-ai/dsh-tool-pty` | `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | `ctx.tools`, `ctx.pty`, `ctx.systemPrompt`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The six terminal tools are opt-in and complement one-shot bash/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.tasks`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. |
| `@deepseek-ai/dsh-tool-goal` | `create_goal`, `get_goal`, `update_goal` | `ctx.tools`, `ctx.agents`, `ctx.goals`, `ctx.systemPrompt`, `a calling Agent in an authorized open turn` | `tool/call`, `context/message goal snapshot for mutations`, `tool/result` | - | create, edit, pause, and resume require direct-human root authority; complete and blocked also accept the exact current goal round. The default blocked lower bound is three admitted rounds. |
| `@deepseek-ai/dsh-tool-goal` | `create_goal`, `get_goal`, `update_goal` | `ctx.tools`, `ctx.agents`, `ctx.goals`, `ctx.systemPrompt`, `a calling Agent in an authorized open turn` | `tool/call`, `user/message goal snapshot for mutations`, `tool/result` | - | create, edit, pause, and resume require direct-human root authority; complete and blocked also accept the exact current goal round. The default blocked lower bound is three admitted rounds. |
| `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.tools`, `ctx.lsp`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | The lsp tool keeps provider selection and language-server subprocesses behind ctx.lsp, so its model-visible schema stays stable across providers. Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-local`) at runtime; without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema. |
| `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflows`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. |
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`, `ctx.skills` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `examples/tui-agent/cordis.yml` and `examples/acp-agent/cordis.yml`. |
| `@deepseek-ai/dsh-tool-tasks` | `task_kill`, `task_list`, `task_output` | `ctx.tools`, `ctx.tasks`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `context/message via agent.inject() for background completion notices` | - | The kind-agnostic background-task control surface: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers' `ctx.tasks.start()`. |
| `@deepseek-ai/dsh-tool-tasks` | `task_kill`, `task_list`, `task_output` | `ctx.tools`, `ctx.tasks`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-task control surface: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers' `ctx.tasks.start()`. |
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan. |
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflows`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. |

View File

@@ -22,7 +22,7 @@ flowchart TD
normalized["Registry outer normalization<br/>pipeline/result snapshot throws become isError"]
finalize["ToolDefinition.finalizeContent<br/>last content-only invariant"]
final["<code>tools/result</code> synchronous notification<br/>frozen authoritative outcome"]
context["Active-batch additionalContexts FIFO<br/>context/message after recorded tool results"]
context["Active-batch additionalContexts FIFO<br/>injected user/message after recorded tool results"]
toolResult["Session event: <code>tool/result</code><br/>single model-facing outcome"]
allResults["Tool batch settled<br/>recorded tool/result events complete"]
presentResult["UI completed card<br/>presentResult(args, result)"]