Переглянути джерело

docs(subsystems): open core.md on agent creation/ownership and the Agent contract; enforce a complete folder index

core.md claimed to be the packages/core reference but opened on repo-wide type patterns and never documented the ownership vocabulary: AgentHandle, CreateAgentOptions, ResumeAgentOptions, and AgentFactory were TYPE_LINK_EXEMPTIONS pointing at a package README, invisible to the folder that calls itself the type reference. The page now reads spine map -> creation and ownership (AgentHandle pasted; the options and factory summarized with links into the generated registry section) -> the Agent handle (AgentStatus, AgentOptions, SteeringOutcome, SteeringReceipt, and SettleReason now pasted; the one settlement prose wall split by topic; delivery vocabulary ordered as a message travels) -> initiator -> interception -> a Sessions summary -> the ToolDefinition pointer -> an explicitly framed repo-wide patterns tail (the ...Map pattern, branded ids). The duplicate SessionEvent paste is gone -- session.md owns it and LINK_MAP follows -- the four ownership types moved from TYPE_LINK_EXEMPTIONS into LINK_MAP -> core.md, and three dead LINK_MAP entries (ContinuationDecision, ContinuationStop, HookContext) no longer name types absent from the source tree. The "what this page owns" meta-section folds into the intro.

The subsystems README index silently lost tasks.md and session-reference.md on both language sides during a base absorption; the rows are restored and scripts/project-doc-site.spec.ts now fails when any page misses either side of the index (proven red on a removed row). tools.md links ToolSchema to its llm-streaming.md declaration instead of calling it core; subagent.md links AgentHandle and CreateAgentOptions.seed to the new section. A new Agent Note records the package-anchored page-scoping decision; the 2026-06-20 catalog note marks its spine-vs-seam rule superseded as the page-scoping rule while keeping the type-equiv mechanism current, and docs/AGENTS.md cites the new note.
Tianyi Cui 2 місяців тому
батько
коміт
a2aa567371
46 змінених файлів з 1059 додано та 967 видалено
  1. 2 2
      .agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml
  2. 2 0
      .agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md
  3. 3 1
      .agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.zh.md
  4. 6 0
      .agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml
  5. 34 0
      .agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md
  6. 34 0
      .agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md
  7. 3 3
      docs/AGENTS.md
  8. 2 2
      docs/subsystems/README.i18n.yaml
  9. 4 3
      docs/subsystems/README.md
  10. 4 3
      docs/subsystems/README.zh.md
  11. 2 2
      docs/subsystems/core.i18n.yaml
  12. 267 427
      docs/subsystems/core.md
  13. 272 428
      docs/subsystems/core.zh.md
  14. 1 1
      docs/subsystems/http-server.i18n.yaml
  15. 1 1
      docs/subsystems/http-server.zh.md
  16. 2 2
      docs/subsystems/llm-streaming.i18n.yaml
  17. 174 19
      docs/subsystems/llm-streaming.md
  18. 174 19
      docs/subsystems/llm-streaming.zh.md
  19. 2 2
      docs/subsystems/permission.i18n.yaml
  20. 1 1
      docs/subsystems/permission.md
  21. 1 1
      docs/subsystems/permission.zh.md
  22. 2 2
      docs/subsystems/persistence.i18n.yaml
  23. 1 1
      docs/subsystems/persistence.md
  24. 1 1
      docs/subsystems/persistence.zh.md
  25. 2 2
      docs/subsystems/session-projection.i18n.yaml
  26. 1 1
      docs/subsystems/session-projection.md
  27. 1 1
      docs/subsystems/session-projection.zh.md
  28. 2 2
      docs/subsystems/session.i18n.yaml
  29. 1 1
      docs/subsystems/session.md
  30. 1 1
      docs/subsystems/session.zh.md
  31. 2 2
      docs/subsystems/subagent.i18n.yaml
  32. 2 2
      docs/subsystems/subagent.md
  33. 2 2
      docs/subsystems/subagent.zh.md
  34. 2 2
      docs/subsystems/tools.i18n.yaml
  35. 1 1
      docs/subsystems/tools.md
  36. 1 1
      docs/subsystems/tools.zh.md
  37. 2 2
      packages/interaction/README.i18n.yaml
  38. 1 1
      packages/interaction/README.md
  39. 1 1
      packages/interaction/README.zh.md
  40. 1 1
      packages/self-modification/README.i18n.yaml
  41. 0 2
      packages/self-modification/README.zh.md
  42. 5 8
      scripts/gen-cordis-catalog.ts
  43. 15 2
      scripts/project-doc-site.spec.ts
  44. 0 0
      scripts/snapshots/translation-prompt-v4/request-response.expected.json
  45. 20 10
      scripts/type-equiv.manifest.json
  46. 1 1
      website/docs.ts

+ 2 - 2
.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md
-2026-06-20-core-data-structures-catalog.md: b358ad54652e5b16ae90037c128cf756bcfdaed3
-2026-06-20-core-data-structures-catalog.zh.md: d84c2befaee4c2032b358ed1a7949ac0151ae74c
+2026-06-20-core-data-structures-catalog.md: 9a3f80df592f28adc365d5e64cc15516d92b47b5
+2026-06-20-core-data-structures-catalog.zh.md: 96c606a3d65a67e757960c5cbc997e717f11e8d5

+ 2 - 0
.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md

@@ -16,6 +16,8 @@ A new `docs/subsystems/` folder catalogs the vocabulary, with a new `verify-type
 
 ### What counts as "core" — the spine-vs-seam line
 
+> **Superseded as the page-scoping rule** by [package-anchored subsystem pages](2026-08-03-package-anchored-subsystem-pages.md): each page now anchors to the package group that declares its vocabulary. The `ts type-equiv` mechanism below remains current.
+
 The scoping line was not picked top-down; it was discovered by testing candidate definitions against concrete borderline types until one rule survived every case. The decisive test was `BashExecRequest`/`BashExecSpec`/`BashRunResult`: bash is a capability *seam*, not part of the agent-loop spine, so if those are "core" then "core" means *all cross-package vocabulary* and the catalog is a flat dump; if they are not, "core" means *the central spine* and bash vocabulary belongs on its own seam page. The latter won, which set the whole structure: a **tiered folder**, not a flat document.
 
 The rule that settled the remaining cases: ***the type you write, hold, or receive is core; the machinery that types it, renders it, or persists it is a seam-page detail.*** Worked through:

+ 3 - 1
.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.zh.md

@@ -16,6 +16,8 @@ Status: implemented
 
 ### 何为「核心」——主干与 seam 的分界线
 
+> **作为页面范围界定规则已被取代**,见[按包锚定的子系统页面](2026-08-03-package-anchored-subsystem-pages.md):每页现在锚定到声明其词汇的包分组。下文的 `ts type-equiv` 机制仍然有效。
+
 范围界定并非自上而下拍定,而是将候选定义逐一对照具体的边界类型反复测试,直到一条规则在所有案例中都成立。决定性的测试是 `BashExecRequest`/`BashExecSpec`/`BashRunResult`:bash 是一个能力 *seam*,不属于 agent loop(智能体循环)主干;如果这些算「核心」,那么「核心」就意味着*所有跨包词汇*,目录沦为平铺罗列;如果不算,「核心」就意味着*中央主干*,bash 词汇归入子页面。后者胜出,由此确定了整体结构:一个**分层文件夹**,而非一份平铺文档。
 
 确定其余案例的规则是:***你编写、持有或接收的类型是核心;为其提供类型推导、渲染或持久化的机制是子页面细节。*** 逐一验证如下:
@@ -25,7 +27,7 @@ Status: implemented
 - `ToolSchema` 是核心(它是流经每个步骤的模型请求 `GenerateOptions` 的一个字段),即使它在概念上属于工具流水线——当*流经主干*与*概念归属*冲突时,前者胜出。
 - 工具展示词汇(`ToolCallView`/`ToolResultView` 等)、`SessionPersistence` 持久性 seam 以及 bash 词汇是子页面。
 
-`core.md` 是一份**自包含的主干文档**:它给出每个主干结构的确切类型定义,辅以最少的行文,并链接到子页面获取各 seam 的细节。子页面包括 `llm-streaming.md`、`session.md`、`persistence.md`(沿内存模型与持久性 seam 的分界线从会话页面拆出)、`tools.md` 和 `bash.md`。
+`core.md` 是一份**自包含的主干文档**:它给出每个主干结构的确切类型定义,辅以最少的行文,并链接到同级 seam 页面获取各 seam 的细节;目录的 [README](../../../../docs/subsystems/README.md) 索引全部页面。最初的 seam 页面包括 `llm-streaming.md`、`session.md`、`persistence.md`(沿内存模型与持久性 seam 的分界线从会话页面拆出)、`tools.md` 和 `bash.md`。
 
 ### `ts type-equiv` 机制——既逐字又防漂移
 

+ 6 - 0
.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml

@@ -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 .agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md
+2026-08-03-package-anchored-subsystem-pages.md: 5ba7937f0c142727d343a107a3ff87f5f337931e
+2026-08-03-package-anchored-subsystem-pages.zh.md: 27d56158a69a8144b0915054032fb3b54493bd0b

+ 34 - 0
.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md

@@ -0,0 +1,34 @@
+# Agent Note: Package-anchored subsystem pages and thin group READMEs
+
+Status: implemented
+
+English | [中文](2026-08-03-package-anchored-subsystem-pages.zh.md)
+
+## Problem
+
+The [subsystems catalog](2026-06-20-core-data-structures-catalog.md) scoped its front page by the spine-vs-seam rule: a type was "core" if the loop holds, derives, streams, or logs it on every turn. That rule selected types, not packages, so as the folder grew to forty-plus pages the front page became a cross-package grab-bag: LLM conversation vocabulary sat above the agent contracts, the creation/ownership vocabulary (`AgentHandle`, `CreateAgentOptions`, `ResumeAgentOptions`, `AgentFactory`) was documented nowhere in the folder because the generator exempted it to a package README, and a reader could not predict which page documents a type from where the type lives. Package-group READMEs meanwhile had no common shape — some carried sectioned tables, stray design essays, or trailing paragraphs that belonged on a subsystem page.
+
+## Decision
+
+Every `docs/subsystems/` page anchors to the package or package group that declares its vocabulary, and page membership follows the repository layout: [core.md](../../../../docs/subsystems/core.md) is the `packages/core` page (creation and ownership, the `Agent` handle with its delivery/cancellation/interception contracts, pointers to the group's dedicated pages), [llm-streaming.md](../../../../docs/subsystems/llm-streaming.md) owns `packages/llm` end-to-end, and so on. Repo-wide type patterns (`…Map → derived-union`, branded ids) stay on core.md in an explicitly framed closing section rather than interleaved with the package content. This supersedes the spine-vs-seam rule *as the page-scoping rule*; the placement heuristic that survives is simpler: a type is documented where its declaring package's page is, and machinery keeps living with its machinery.
+
+Every type a generated signature references must resolve somewhere in the folder: the agent ownership vocabulary moved from the generator's `TYPE_LINK_EXEMPTIONS` into `LINK_MAP → core.md`, so exemptions are reserved for genuinely service-local or vendored shapes. Each pasted declaration has one home (`SessionEvent` lives on [session.md](../../../../docs/subsystems/session.md); core.md summarizes and links).
+
+Every `packages/<group>/README.md` pair is a thin front door in one shape: a why-first intro paragraph, a package table (Package / Role / ctx key), and a closing pointer to the owning subsystems page. Load-bearing prose that outgrows that shape relocates to the owning subsystems page rather than being deleted.
+
+The [subsystems README](../../../../docs/subsystems/README.md) indexes every page in the folder on both language sides; `scripts/project-doc-site.spec.ts` enforces one table row per page, so a page added by a later PR (or absorbed in a merge) cannot silently miss the index.
+
+## Alternatives considered
+
+**Keep the spine-vs-seam scoping rule.** It answered "is this type core?" per type, which is why the front page accumulated types from four packages while missing half of `packages/core/agent`'s public surface. Predictability by repository layout won.
+
+**A flat single-document catalog.** Already rejected in the [original catalog note](2026-06-20-core-data-structures-catalog.md); the growth to forty-one pages confirmed that verdict.
+
+**Document ownership vocabulary only in package READMEs (the exemption status quo).** This left `AgentHandle` and the create/resume options invisible to the folder that claims to be the type reference, and the generated `Types:` footers could not link them.
+
+## Consequences
+
+- Which page documents a type is predictable from `packages/<group>/`; the subsystems README is a complete index enforced by test.
+- Generated signature footers link the agent ownership vocabulary instead of silently exempting it.
+- `verify-type-equiv`'s 1:1 manifest keeps each paste single-homed; the duplicate `SessionEvent` paste is gone.
+- The [original catalog note](2026-06-20-core-data-structures-catalog.md) remains the owner of the `ts type-equiv` drift-gate mechanism; only its page-scoping rule is superseded here.

+ 34 - 0
.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md

@@ -0,0 +1,34 @@
+# Agent Note:按包锚定的子系统页面与轻薄的分组 README
+
+Status: implemented
+
+[English](2026-08-03-package-anchored-subsystem-pages.md) | 中文
+
+## Problem
+
+[子系统目录](2026-06-20-core-data-structures-catalog.md)最初用主干-vs-seam 规则界定首页范围:如果循环在每个轮次都持有、派生、流式传输或记录某个类型,它就是「核心」。该规则选择的是类型而非包,因此当目录增长到四十多页后,首页变成了跨包大杂烩:LLM 对话词汇排在 agent 契约之前,创建/所有权词汇(`AgentHandle`、`CreateAgentOptions`、`ResumeAgentOptions`、`AgentFactory`)在目录中无处记录(生成器把它们豁免给了某个包 README),读者无法根据类型所在位置预测哪一页记录它。与此同时,各包分组 README 没有统一形状——有的带分节表格、游离的设计短文,或本应属于子系统页面的尾部段落。
+
+## Decision
+
+每个 `docs/subsystems/` 页面锚定到声明其词汇的包或包分组,页面归属跟随仓库布局:[core.md](../../../../docs/subsystems/core.md) 是 `packages/core` 的页面(创建与所有权、`Agent` 句柄及其投递/取消/拦截契约、指向该组专属页面的指针),[llm-streaming.md](../../../../docs/subsystems/llm-streaming.md) 端到端拥有 `packages/llm`,依此类推。全仓通用类型模式(`…Map → 派生联合`、品牌化 id)保留在 core.md 一个明确标注的收尾小节中,而不是与包内容交错。这在*页面范围界定规则*的意义上取代了主干-vs-seam 规则;存活下来的放置启发式更简单:类型记录在其声明包对应的页面,机制仍与其机制放在一起。
+
+生成签名引用的每个类型都必须能在目录中某处解析:agent 所有权词汇从生成器的 `TYPE_LINK_EXEMPTIONS` 移入 `LINK_MAP → core.md`,因此豁免只留给真正服务本地或 vendored 的形状。每个粘贴的声明只有一个家(`SessionEvent` 位于 [session.md](../../../../docs/subsystems/session.md);core.md 概括并链接)。
+
+每个 `packages/<group>/README.md` 配对都是统一形状的轻薄门面:一段以「为什么」开头的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向拥有方子系统页面的收尾指针。超出该形状的承重散文迁移到拥有方子系统页面,而非删除。
+
+[子系统 README](../../../../docs/subsystems/README.md) 在两个语言侧索引目录中的每一页;`scripts/project-doc-site.spec.ts` 强制每页一行表格,因此后续 PR 新增(或合并吸收)的页面无法悄悄缺席索引。
+
+## Alternatives considered
+
+**保留主干-vs-seam 界定规则。**它逐类型回答「这个类型是核心吗?」,这正是首页积累了四个包的类型、却缺失 `packages/core/agent` 一半公开 surface 的原因。按仓库布局可预测性胜出。
+
+**扁平的单文档目录。**在[原目录 note](2026-06-20-core-data-structures-catalog.md) 中已被否决;增长到四十一页证实了该结论。
+
+**只在包 README 中记录所有权词汇(豁免的现状)。**这让 `AgentHandle` 与 create/resume 选项在自称类型参考的目录中不可见,生成的 `Types:` 页脚也无法链接它们。
+
+## Consequences
+
+- 哪一页记录某类型可由 `packages/<group>/` 预测;子系统 README 是由测试强制的完整索引。
+- 生成的签名页脚链接 agent 所有权词汇,而不是静默豁免。
+- `verify-type-equiv` 的 1:1 manifest 保证每个粘贴单一归属;重复的 `SessionEvent` 粘贴已移除。
+- [原目录 note](2026-06-20-core-data-structures-catalog.md) 仍拥有 `ts type-equiv` 漂移检查机制;此处仅取代其页面范围界定规则。

+ 3 - 3
docs/AGENTS.md

@@ -21,8 +21,8 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home.
 | Root `AGENTS.md` | Standing orders: rules an agent needs in context in every session, one to three lines each, linking its home | Stories, worked examples, situational procedures, anything restated from a linked home |
 | Subtree `AGENTS.md` (`packages/`, `examples/`, `docs/`, `.agents/notes/`) | Orders specific to that subtree | Repo-wide rules the root file already carries |
 | [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ subsystems), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations |
-| [subsystems/](subsystems/README.md) | One reference page per subsystem: literal type shapes, semantics, and the generated Cordis surface | Behavior narration (→ architecture.md) |
-| [Agent Notes](../.agents/notes/README.md) | Active decision records: the why, what-was-given-up, and concise verification contract; `implemented/` notes describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped; archived notes are frozen history, never current authority |
+| [subsystems/](subsystems/README.md) | One reference page per subsystem: type shapes, semantics, and the generated Cordis surface | Behavior narration (→ architecture.md) |
+| [Agent Notes](../.agents/notes/README.md) | Active decision records: the why, what-was-given-up, and verification contract; `implemented/` notes describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped; archived notes are frozen history, never current authority |
 | [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
 | [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the Agent Note each guide links) |
 | [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
@@ -39,7 +39,7 @@ Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookb
 - **Every non-trivial change includes at least one Agent Note in the same PR.** Update the owning note or add one; only mechanical/local edits are exempt ([scope](../.agents/notes/README.md#when-to-write-one)).
 - **One physical line per paragraph** (`verify-md-wrap`): use editor soft-wrap. Code blocks, tables, and list structure keep their formatting; code comments stay under the linter's column limit.
 - **Fenced `ts` blocks must compile** (`doc-typecheck`); a pasted type declaration and its original JSDoc use ` ```ts type-equiv `, while a body-stripped public class declaration uses ` ```ts public-api `; register either in the manifest so neither can drift ([mechanics](development.md#documenting-types-verbatim-ts-type-equiv)).
-- **The owning [subsystems page](subsystems/README.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types ([what counts as core](subsystems/core.md#what-counts-as-core)).
+- **The owning [subsystems page](subsystems/README.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types; a type is documented on its declaring package group's page ([page scoping](../.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md)).
 - **Bilingual pairs update together**: editing either side obligates the counterpart and a re-record in the same change ([i18n contract](i18n/README.md)).
 - **Comments and JSDoc state complete contracts, not reasoning transcripts.** Preserve behavior, timing, modality, exceptions, consequences, and non-obvious orientation; delete narration, test walkthroughs, review analysis, and code restatement. Keep the local contract and link its rationale. Use [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for details.
 - Your audience is professional programmers. Prefer concise and straight-forward English over metaphor. Do not overuse words like "gate", "vocabulary", "surface", "seams".

+ 2 - 2
docs/subsystems/README.i18n.yaml

@@ -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 docs/subsystems/README.md
-README.md: d225e5f4eeb31d1e6c71cbd8311e28bae1e8b3a5
-README.zh.md: 9f78ac3e1c79440b053ee4e344dbdba2c11b3dfc
+README.md: 1c3d14aeb4fd11cbc45eaadd83f0b84de0dfda52
+README.zh.md: dda61d86c4d00117af851b4a55d16b97c6bc2f10

+ 4 - 3
docs/subsystems/README.md

@@ -6,7 +6,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
 
 | Page | Owns |
 |---|---|
-| [core.md](core.md) | the `packages/core` control spine: the package-by-package loop map, the `Agent` handle with its delivery/cancellation/interception contracts, the `SessionEvent` envelope, branded ids, the `…Map → derived-union` pattern |
+| [core.md](core.md) | the `packages/core` control spine: the package-by-package loop map, agent creation and ownership (`AgentHandle`), the `Agent` handle with its delivery/cancellation/interception contracts, and the repo-wide type patterns (`…Map → derived-union`, branded ids) |
 | [llm-streaming.md](llm-streaming.md) | the `packages/llm` conversation vocabulary — `Message`/`ContentBlock`, the assembled model request, the `StreamChunk` wire protocol + adapter contract, `BlockAssembler`, the `LlmAdapter` seam |
 | [token-meter.md](token-meter.md) | immutable scalar and positional replay measurements with consumed-log revisions |
 | [scope.md](scope.md) | scoped registration identity, dispatch carriers, and the owned `Scope` context |
@@ -19,6 +19,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
 | [credentials.md](credentials.md) | the credential seam: `CredentialRef` references (never values) in configuration, per-operation resolution, UI-safe `CredentialInfo`, provider source layers |
 | [session-query.md](session-query.md) | logical records, bounded exact-event reads, relationship traces, semantic filters/documents, and full-text result pages |
 | [session-title.md](session-title.md) | durable title snapshots, source provenance, and the asynchronous provider contract |
+| [session-reference.md](session-reference.md) | structured cross-session references: `SessionReferenceInput`/`Candidate`, prepared message contexts, the stable error taxonomy |
 | [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, prompt sections, and cooperative assembly |
 | [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, and the guarded execution pipeline |
 | [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
@@ -36,12 +37,12 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
 | [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` |
 | [spill.md](spill.md) | the spill storage seam: `SaveTextSpill`, `SpillOwner`/`SpillSource`, `SpillRef`, the branded `SpillLocator` |
 | [workflow.md](workflow.md) | the workflow seam: `WorkflowStartRequest`, `WorkflowMeta`, `WorkflowRun`/`Result`, the `workflow/*` event payloads, `WorkflowError` fatality |
+| [tasks.md](tasks.md) | the background-task runtime: branded `TaskId`s, the producer contract, consumer views, `ctx.tasks` service behavior |
 | [permission.md](permission.md) | the permission-preset layer: `PresetSpec`/`PresetOption`, the derived `custom` state, the log-only `permission/preset` event |
 | [plan.md](plan.md) | plan mode: the log-only `plan/mode` state, pending-selection flush, `PlanModeConfig`, the `exit_plan_mode` review arc |
 | [invariants.md](invariants.md) | the runtime-invariant registry: selection `Config`, `InvariantInstaller`/`InvariantFailure`, the empty-companion contract |
-| [http-server.md](http-server.md) | the HTTP carrier: `WebRouteKind`/`WebRoute`, match order, the static dist fallback, index taps |
+| [http-server.md](http-server.md) | the HTTP carrier: `WebRouteKind`/`WebRoute`, match order, the claimable fallback seat, index taps |
 | [storage.md](storage.md) | the storage subsystem: the backend seam (`StorageBackend`), `StorageForms`, `DomainSpec`/`Domain`, `domain/changed` |
-| [tui.md](tui.md) | the terminal-extension seam: `TuiOverlayRequest`/`Host`/`Session`, close reasons and outcomes, the modal queue |
 | [workspace.md](workspace.md) | the workspace registry: `Workspace`/`WorkspaceId`, registration and resolution, the session `cwd` relationship |
 | [client-modules.md](client-modules.md) | the web plugin table: `dshClient` declarations, `WebBootGraph` wire composition, the bundle route and index tap |
 | [session-projection.md](session-projection.md) | the projection seam: `SessionProjectionMap`, the pure `ProjectionDefinition` unit, `ProjectionSnapshot`'s consistent cut, the change feed |

+ 4 - 3
docs/subsystems/README.zh.md

@@ -6,7 +6,7 @@
 
 | 页面 | 负责内容 |
 |---|---|
-| [core.md](core.md) | `packages/core` 控制主干:逐包循环地图、`Agent` 句柄及其投递/取消/拦截契约、`SessionEvent` 信封、品牌化 id,以及 `…Map → 派生联合` 模式 |
+| [core.md](core.md) | `packages/core` 控制主干:逐包循环地图、agent 创建与所有权(`AgentHandle`)、`Agent` 句柄及其投递/取消/拦截契约,以及全仓通用类型模式(`…Map → 派生联合`、品牌化 id) |
 | [llm-streaming.md](llm-streaming.md) | `packages/llm` 的对话词汇——`Message`/`ContentBlock`、组装完成的模型请求、`StreamChunk` 协议格式(wire format)+ 适配器契约(adapter contract)、`BlockAssembler`、`LlmAdapter` seam |
 | [token-meter.md](token-meter.md) | 不可变的标量与位置回放度量,附带已消费日志修订号 |
 | [scope.md](scope.md) | 作用域注册标识、dispatch 载体,以及拥有的 `Scope` 上下文 |
@@ -19,6 +19,7 @@
 | [credentials.md](credentials.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、provider 来源层 |
 | [session-query.md](session-query.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 |
 | [session-title.md](session-title.md) | 持久标题快照、来源 provenance 与异步提供方契约 |
+| [session-reference.md](session-reference.md) | 结构化跨会话引用:`SessionReferenceInput`/`Candidate`、prepared 消息上下文、稳定错误分类 |
 | [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、错误分类体系 |
@@ -36,12 +37,12 @@
 | [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` 致命性 |
+| [tasks.md](tasks.md) | 后台任务运行时:品牌化 `TaskId`、producer 契约、consumer 视图、`ctx.tasks` 服务行为 |
 | [permission.md](permission.md) | 权限预设层:`PresetSpec`/`PresetOption`、派生的 `custom` 状态、仅记日志的 `permission/preset` 事件 |
 | [plan.md](plan.md) | 计划模式:仅记日志的 `plan/mode` 状态、待定选择的冲刷、`PlanModeConfig`、`exit_plan_mode` 审阅流程 |
 | [invariants.md](invariants.md) | 运行时不变式注册表:选择配置 `Config`、`InvariantInstaller`/`InvariantFailure`、空配套插件契约 |
-| [http-server.md](http-server.md) | HTTP 载体:`WebRouteKind`/`WebRoute`、匹配顺序、静态 dist 回退、index 转换 |
+| [http-server.md](http-server.md) | HTTP 载体:`WebRouteKind`/`WebRoute`、匹配顺序、可认领的回退席位、index 转换 |
 | [storage.md](storage.md) | 存储子系统:后端 seam(`StorageBackend`)、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed` |
-| [tui.md](tui.md) | 终端扩展 seam:`TuiOverlayRequest`/`Host`/`Session`、关闭原因与结果、模态队列 |
 | [workspace.md](workspace.md) | 工作区注册表:`Workspace`/`WorkspaceId`、注册与解析、与会话 `cwd` 的关系 |
 | [client-modules.md](client-modules.md) | Web 插件表:`dshClient` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
 | [session-projection.md](session-projection.md) | 投影 seam:`SessionProjectionMap`、纯函数 `ProjectionDefinition` 单元、`ProjectionSnapshot` 的一致切面、变更馈送 |

+ 2 - 2
docs/subsystems/core.i18n.yaml

@@ -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 docs/subsystems/core.md
-core.md: ba9319d377cf250148a145ba3f1fa1baa967c858
-core.zh.md: 276dc4f99f1f802e66aca050b2fbcdc178931611
+core.md: cc9cc30677b0b657492d8a5118489e8b801f3fd3
+core.zh.md: 6878300dc2cbd82d220e17d2d6f5e1009f41c48f

+ 267 - 427
docs/subsystems/core.md

@@ -2,11 +2,11 @@
 
 English | [中文](core.zh.md)
 
-The **core** subsystem is [`packages/core`](../../packages/core/README.md) — the control spine every composition boots: the event-sourced session log, system-prompt assembly, the tool registry, the agent vocabulary, and the concrete loop that drives them. This page owns the spine's shared vocabulary — the `Agent` handle, its delivery and interception contracts, and the repo-wide type patterns — and orients to the group's dedicated pages; the folder is indexed in the [subsystems README](README.md).
+The **core** subsystem is [`packages/core`](../../packages/core/README.md) — the control spine every composition boots: the event-sourced session log, system-prompt assembly, the tool registry, the agent vocabulary, and the concrete loop that drives them. This page owns what the `agent`/`agent-loop` pair declares — how an agent is created and owned, and the `Agent` handle with its delivery, cancellation, and interception contracts — plus the two type patterns every subsystem follows; the group's dedicated pages and the rest of the folder are indexed in the [subsystems README](README.md).
 
 ## The spine, package by package
 
-A turn flows through the six packages in one loop: the driver in [`agent-loop`](../../packages/core/agent-loop) claims a queued prompt, opens a turn on the [session log](session.md) (`ctx.sessions`), assembles the request prefix through [system-prompt](system-prompt.md) (`ctx.systemPrompt`) and derives history from the log, streams the model response through the [LLM seam](llm-streaming.md), dispatches tool calls through the [tool registry](tools.md) (`ctx.tools`), and appends every model-visible fact back onto the log before the next step derives from it.
+A turn flows through the six packages in one loop: the driver in [`agent-loop`](../../packages/core/agent-loop) claims a queued prompt, opens a turn on the [session log](session.md) (`ctx.sessions`), assembles the request prefix through [system-prompt](system-prompt.md) (`ctx.systemPrompt`) and derives history from the log, streams the model response through the [LLM seam](llm-streaming.md), dispatches tool calls through the [tool registry](tools.md) (`ctx.tools`), and appends every model-visible fact back onto the log before the next step derives from it. The conversation vocabulary the loop moves — `Message`, `ContentBlock`, `StreamChunk`, the model request — is declared by [`packages/llm`](../../packages/llm/README.md) and documented on [llm-streaming.md](llm-streaming.md).
 
 | Package | Owns | Page |
 |---|---|---|
@@ -19,208 +19,45 @@ A turn flows through the six packages in one loop: the driver in [`agent-loop`](
 
 `scope/` is the one non-service package: a dependency-free library (`createScope`/`scopeOf`/`scopeTarget`) that sits below `session/` and `system-prompt/` in the module graph precisely so they can consume it without a cycle. `agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop; it runs each driver inside `ctx.agents.withInitiator()`. Extension plugins depend on `agent` — including when they need the initiating Agent — and never on `agent-loop` directly, so the loop stays swappable. The default composition that wires this spine into a runnable agent is [`examples/agent-spine-demo`](../../packages/examples/agent-spine-demo/README.md).
 
-<a id="what-counts-as-core"></a>
+## Creation and ownership
 
-## What this page owns
+Consumers create agents through `ctx.agents` — `create()` builds a fresh session and agent under one caller-supplied `SessionId`, `resume()` loads a persisted session first — or declaratively through the loop's config entries. Programmatic creation returns the owner's handle:
 
-The conversation vocabulary the loop moves — `Message`, `ContentBlock`, `StreamChunk`, the model request — is declared by [`packages/llm`](../../packages/llm/README.md) and documented on [llm-streaming.md](llm-streaming.md); the session-event, prompt-assembly, and tool vocabularies live on this group's dedicated pages above. What remains here is the vocabulary shared by everything: the `Agent` handle and its delivery, cancellation, and interception contracts (declared by `packages/core/agent`), the `SessionEvent` envelope, and the two type patterns every subsystem follows. The scoping rule is recorded in the [subsystems-catalog Agent Note](../../.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md): the type you write, hold, or receive is documented where its declaring subsystem is; the machinery that types, renders, or persists it stays on that machinery's page.
-
-## The `…Map → derived-union` pattern
-
-Almost every extensible sum type in the harness follows one shape: an interface keyed by a discriminant tag (the `…Map`), from which the union is derived with `keyof`. Plugins add variants by **declaration merging** — no edit to the owning 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'; /* … */ }
-  }
-}
-```
-
-Six canonical maps use this pattern; a plugin author extends these:
-
-| Map | Package | Derives | Catalog |
-|---|---|---|---|
-| `ContentBlockMap` | dsh-llm | `ContentBlock` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) |
-| `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) |
-| `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.md#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) |
-
-Two large discriminated unions are the ones consumers `switch` over most: **`StreamChunk`** (the streaming protocol) and **`SessionEvent`** (the log entry). Per the repo convention, `switch` on the tag — don't chain `if`s — so each arm narrows and a typo'd tag fails to compile.
-
-## Branded IDs
-
-IDs that cross package boundaries are **branded** — structurally strings, but non-interchangeable at the type level (a `SessionId` cannot be passed where a `CallId` is expected). Construction goes through a per-type factory; comparison, logging, and JSON behave as ordinary strings.
-
-The `Branded<B>` primitive lives in its own type-only package, [dsh-brand](../../packages/util/brand) (no runtime code, no harness-package dependency), so any package can brand the ids it owns without depending on an unrelated capability package.
-
-Source: [`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 }
-```
-
-The two core IDs are `CallId` (correlates a tool call with its result; dsh-llm) and `SessionId` (the shared live agent and durable session identity; dsh-session). Capability packages brand their own ids too, such as `TaskId` in [tasks.md](tasks.md).
-
-## Sessions
-
-A `Session` is an **append-only log** of typed `SessionEvent`s — the single source of truth. The LLM message history is *derived* from the log (`deriveMessages()`), not stored separately. The event vocabulary derives from `SessionEventMap`:
-
-Source: [`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]
-```
-
-The twelve event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/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 execution-enclosure and standalone-event rules 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
-
-`Agent` is the surface every plugin (UI, hooks, orchestrators) programs against. The concrete implementation is package-internal to dsh-agent-loop; nothing outside the loop depends on it.
-
-Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
-
-```ts type-equiv
-/**
- * Which inbox queue a {@link Agent.send} item joins:
- * - `next-turn` — the item becomes its own turn, claimed at a turn boundary.
- * - `next-step` — during prompt admission or an open turn, the item stages for
- *   the next safe step boundary; otherwise it is promoted per its `wakeup`
- *   flag.
- */
-type SendTarget = 'next-turn' | 'next-step'
-```
-
-```ts type-equiv
-/** Resolved inbox placement reported when an accepted message is enqueued. */
-type InboxPlacement = 'queued' | 'steering'
-```
-
-`InboxItemId` is a process-local branded string minted for each accepted FIFO occurrence. It is intentionally distinct from `MessageId`: sending the same immutable message twice creates two independently addressable pending items.
-
-```ts type-equiv
-/** One independently addressable accepted occurrence in an agent inbox. */
-interface InboxItem {
-  /** Agent-loop-minted occurrence identity. */
-  readonly id: InboxItemId
-  /** Identified message delivered by the caller. */
-  readonly message: UserMessage
-  /** Acceptance-time FIFO classification. */
-  readonly placement: InboxPlacement
-}
-```
-
-```ts type-equiv
-/** A user-requested mutation of one still-pending queued occurrence. */
-type InboxAction =
-  | { readonly kind: 'edit'; readonly content: ContentBlock[] }
-  | { readonly kind: 'remove' }
-  | { readonly kind: 'steer' }
-```
-
-```ts type-equiv
-/** Result of applying an inbox action at the synchronous ownership boundary. */
-type InboxActionResult = 'applied' | 'not-found' | 'steer-unavailable'
-```
+Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index.ts)
 
 ```ts type-equiv
 /**
- * Options for the unified {@link Agent.send} primitive over the
- * (`target` × `wakeup`) matrix. Named presets: {@link Agent.followup}
- * (`next-turn`/wakeup), {@link Agent.steer} (`next-step`/wakeup), and
- * {@link Agent.inject} (`next-step`/no-wakeup).
+ * An owned agent plus its disposer, returned by {@link AgentRegistry.create} /
+ * {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers,
+ * only the holder can tear this agent down. The registered factory provider is
+ * also a structural owner because the scoped agent depends on that provider's
+ * service surface; provider unload stops and drains every live handle it made.
+ * `dispose()` stops the loop, awaits its exit, unregisters the agent, removes
+ * its session from the store, and finally unwinds its scoped world.
  *
- * The object is complete so routing policy is explicit.
- */
-interface SendOptions {
-  /** Queue the item joins. */
-  target: SendTarget
-  /**
-   * Whether this item makes the model run: wake a parked driver (`next-turn`)
-   * or force a continuation step (`next-step` while running). A `false`
-   * `next-turn` item queues without waking; a `false`
-   * `next-step` item attaches durable context without forcing another step
-   * (the injection preset).
-   */
-  wakeup: boolean
+ * `ctx.agents.get(id)` still returns a bare {@link Agent} — the handle is
+ * exposed only to the consumer owner that created it; the structural provider
+ * reaches the same teardown internally. Config-created agents (the loop's own
+ * startup) are owned by the loop fiber and never need a handle.
+ */
+interface AgentHandle {
+  agent: Agent
+  dispose(): Promise<void>
 }
 ```
 
-The fixed-preset aliases own `target` and `wakeup`; their already identified `UserMessage` carries role, content, and provenance. Its `MessageId` remains stable when an edit replaces content or strict steer transfers the immutable message. The original queued occurrence ends and strict steer accepts a new steering occurrence with a distinct `InboxItemId`. Injection bypasses the FIFOs and never appears on inbox lifecycle events.
+`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, seed boundary, origin classification, delegation depth), an optional `seed` replay prefix for forks, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) composes the agent's scoped world while both ids are still unpublished — everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly — and may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal rolls the transaction back without publishing either id.
 
-```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
-}
-```
+`AgentFactory` is the creation seam behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers program against `ctx.agents` without depending on the concrete loop package. The exact `create`/`resume` signatures and their rollback contracts are in the [generated section](#ctxagents--agentregistry) below.
 
-`SteeringReceipt.outcome` always resolves. `admitted` identifies the turn and step whose immutable request history contains that exact message; `rejected` means lifecycle or terminal policy discarded it first. Synchronous input validation still throws from `steer()`.
+## The agent handle
 
-```ts type-equiv
-/** Stable runtime cause accepted by {@link Agent.cancel}. */
-type AgentCancelCause =
-  | { readonly kind: 'user' }
-  | { readonly kind: 'parent' }
-```
+`Agent` is the surface every plugin (UI, hooks, orchestrators) programs against; `ctx.agents.get(id)` returns it, and the [initiator scope](#initiating-agent) carries it. The concrete implementation is package-internal to dsh-agent-loop; nothing outside the loop depends on it. The unified `send` method exposes target and wakeup routing directly; `followup`, `steer`, and `inject` are fixed-preset aliases.
 
-`Agent` is an interface over the public live-agent contract. Concrete drivers own the `followup`/`steer`/`inject` aliases and route them through `send`'s (`target` × `wakeup`) matrix.
+Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
 
 ```ts type-equiv
-/**
- * Public live-agent handle with aliases over the unified delivery primitive.
- * @typert object
- */
+/** Public live-agent handle. */
 interface Agent {
   /** The single identity shared with {@link session}. */
   readonly id: SessionId
@@ -228,78 +65,28 @@ interface Agent {
   readonly options: AgentOptions
   /** The live session this agent drives; its log is the durable source of truth. */
   readonly session: Session
+  /** The agent-owned projection of durable pending work. */
+  readonly inbox: Inbox
   /** The current lifecycle state, mirrored on every `agent/status` transition. */
   readonly status: AgentStatus
-  /**
-   * Whether a `next-step` send currently stages for prompt admission or the
-   * open turn. Unlike {@link status}, this excludes admission exit and turn
-   * settlement, when a waking `next-step` send becomes a queued follow-up.
-   */
-  readonly acceptsNextStep: boolean
   /** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
   readonly ctx: Context
 
-  /**
-   * The unified delivery primitive over the (`target` × `wakeup`) matrix.
-   * It routes the caller's typed content and source as follows:
-   *
-   * - `next-turn` queues an item that becomes the sole ordinary message of its
-   *   own FIFO-ordered turn; `wakeup:true` wakes a
-   *   parked driver, while `wakeup:false` queues without waking.
-   * - `next-step` with `wakeup:true` stages steering during prompt admission
-   *   or an open turn; outside that window it falls back to a woken
-   *   `next-turn`.
-   * - `next-step` with `wakeup:false` injects durable model-facing context
-   *   without running the model: admission or an open turn stages it for the
-   *   next safe log position, while an injection outside that window appends
-   *   immediately without opening a turn. If admission closes without a turn,
-   *   a context-only boundary appends immediately; context staged beside
-   *   steering remains pending with it.
-   * The agent publishes or queues the identified frozen message as-is.
-   * @param message - identified model-facing content and its producer provenance.
-   * @param options - target queue and wakeup decision.
-   */
-  send(message: UserMessage, options: SendOptions): void
-
-  /**
-   * Reserve admission of the next ordinary turn while this agent is idle, so an
-   * operation can mutate durable history before any queued prompt derives a
-   * request from it. Already-accepted waking work has right of way, including a
-   * send whose wake is still a pending microtask. Later sends keep their
-   * ordinary placement, FIFO order, and `wakeup` facts, and
-   * {@link acceptsNextStep} stays `false`, so a waking `next-step` send becomes
-   * a queued follow-up rather than steering; cancellation and disposal may
-   * still discard them. {@link inject} is not withheld. {@link whenIdle} treats
-   * a live reservation as activity, while lifecycle teardown does not await it.
-   * @returns the idempotent release, or `undefined` when the agent is running, already reserved, or already committed to waking work.
-   */
-  reserveTurnAdmission(): (() => void) | undefined
-
-  /**
-   * Mutate one still-pending queued occurrence synchronously. Editing preserves
-   * the message identity and queue position; removal publishes its terminal
-   * discard. Steer strictly transfers the message into the current next-step
-   * window, or returns `steer-unavailable` without changing the queued
-   * occurrence. Steering occurrences and driver-claimed items return
-   * `not-found`.
-   * @param id - independently addressable queued occurrence.
-   * @param action - edit, remove, or strict steer operation.
-   * @returns the applied outcome or the reason no mutation occurred.
-   */
-  updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult
-
   /**
    * 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. Idle
-   * cancellation is a no-op and does not arm later work.
-   * @param cause - the stable caller intent carried by the current turn signal.
+   * turn or between-turn task. The first cause wins for that activity. With no
+   * active activity, cancellation is a no-op and does not arm later work.
+   * @param cause - the stable caller intent carried by the active operation 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. */
+  /**
+   * Resolve after the current whole-agent activity reaches quiescence. This
+   * follows replacement work started before the observed driver retires,
+   * but does not identify the settlement of any particular message.
+   * @returns fulfillment after no active driver or maintenance task remains.
+   */
   whenIdle(): Promise<void>
 
   /**
@@ -334,35 +121,86 @@ interface Agent {
   followup(message: UserMessage): void
 
   /**
-   * Submit steering with a message-owned admission receipt — the
-   * `next-step`/wakeup preset of {@link send}. During prompt admission or an
-   * open turn, the message waits in the steering FIFO until a committed step
-   * snapshots it; outside that window it enters the ordinary queued FIFO. The
-   * receipt resolves `admitted` only after the message joins that step's
-   * immutable request history, or `rejected` when terminal policy,
-   * cancellation, or disposal discards it first. A non-terminal turn close may
-   * leave it staged for a later admitted prompt without settling the receipt.
+   * Submit steering for the nearest step. An idle driver starts a turn;
+   * a running driver consumes it at its next step boundary.
+   * A rejected step leaves steering parked in the inbox until the next
+   * wake; cancellation or disposal may discard pending steering.
    * @param message - identified steering content and its producer provenance.
-   * @returns the receipt for this exact message's eventual admission outcome.
    */
-  steer(message: UserMessage): SteeringReceipt
+  steer(message: UserMessage): void
 
   /**
-   * Append model-facing context without running the model — the
-   * `next-step`/no-wakeup preset of {@link send}. Admission or an open turn
-   * stages it at the next safe log position; outside that window it appends
-   * immediately without opening a turn. If admission closes without a turn,
-   * a context-only boundary appends immediately; context staged beside
-   * steering remains pending with it.
+   * Queue model-facing context for the next pre-step without waking the
+   * driver. A running driver claims it at the nearest later step boundary;
+   * idle drivers leave it pending until follow-up or steering
+   * wakes them. It may miss a request whose pre-step already claimed its
+   * batch. Cancellation or disposal may discard pending context.
    * @param message - identified injected context and its producer provenance.
    */
   inject(message: UserMessage): void
 }
 ```
 
-`AgentStatus` is `'idle' | 'running'`, and `SessionId` is branded. Disposal removes the agent from the registry and emits `agent/disposed`; it is not a terminal status value. `running` describes the driver-wide drain interval and may span consecutive queued turns; it does not prove a turn is still open. `acceptsNextStep` is the narrower routing predicate for callers that must choose between steering the current admission/turn and submitting a fresh admitted prompt. A live turn-admission reservation is quiescence-relevant without changing `status` or turning later queue entries into steering; its only authority is to defer the next driver claim until release. `AgentOptions` is merge-extensible: core declares `provider?`, `model?`, and `maxTokens?` (dispatch requires provider and model after `agent/request`). When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
+```ts type-equiv
+/**
+ * An agent's lifecycle state, emitted on every transition as `agent/status`:
+ * `idle` means no driver is active; `running` begins when waking input starts
+ * cancellable pre-step processing and lasts while the driver drains,
+ * closes, or checkpoints turns. Disposal removes the agent from its registry;
+ * it is not a third observable status.
+ */
+type AgentStatus = 'idle' | 'running'
+```
+
+`running` describes the driver-wide drain interval and may span consecutive queued turns; it does not prove a turn is still open. Disposal removes the agent from the registry and emits `agent/disposed`; it is not a terminal status value. `followup()` returns no handle: its `MessageId` identifies durable inbox insertion, claim, and discard facts, not a later assistant output or turn ending. `whenIdle()` observes the whole agent, so callers may call a receipt-to-idle interval a run only when they explicitly own that interval ([decision](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md)).
+
+```ts type-equiv
+/** Merge-extensible agent creation options. Persona belongs to system-prompt sections. */
+interface AgentOptions {
+  /** Provider route (must have a registered adapter at call time). */
+  provider?: string
+  /** Model id interpreted by the selected provider adapter. */
+  model?: string
+  /** Maximum output tokens for each conversation-model request. */
+  maxTokens?: number
+}
+```
+
+Dispatch requires `provider` and `model` after `agent/request`. When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. An agent-scoped `deployment:persona` prompt section may shadow the global default persona.
+
+The inbox is the delivery vocabulary — two ordered pending-message lists the agent owns as a durable projection:
+
+```ts type-equiv
+/** One of the two ordered pending-message lists owned by an agent. */
+type InboxTarget = 'next-turn' | 'next-step'
+```
+
+Every pending occurrence is its `UserMessage`; `MessageId` is the sole identity. `Inbox.append`, `prepend`, `replace`, `remove`, `clear`, `splice`, and `claim` record normalized durable `agent/inbox/spliced` mutations and reject duplicate pending ids. `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists; replacement may change identity and emits the old message as discarded followed by the new message as inserted. Ordinary removals and `clear()` are cancellations. `claim(target)` removes the proposed step batch — all `next-step` input plus, at a turn boundary, one `next-turn` message — through pure deletion splices without emitting discarded notifications, and the loop separately emits per-message claimed notifications. Whole-queue consumers such as UI projections reconstruct `nextTurn` and `nextStep` from the durable splices, while consumers following one message use the exact `agent/inbox/inserted`, `claimed`, and `discarded` notifications.
+
+Cancellation:
+
+```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 canceled inbox splice is logged.
+   */
+  keepInbox?: boolean | undefined
+}
+```
+
+```ts type-equiv
+/** Why an active agent driver was cancelled. */
+type AgentCancelCause =
+  | { readonly kind: 'user' }
+  | { readonly kind: 'parent' }
+  | { readonly kind: 'hook'; readonly reason: string }
+  | { readonly kind: 'disposed' }
+```
 
-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. Only the loop reads the cause (`user`, `parent`, or lifecycle-only `disposed`) back off its own machine-private signal at settlement — there is no public reader, and a signal grants cooperating listeners no classification authority. 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 cancellation holder copies it into the runtime-only `AbortSignal.reason`; a signal grants cooperating listeners no classification authority. 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.
 
@@ -372,22 +210,19 @@ The process-local initiator carried by `ctx.agents` is the exact `Agent` above,
 
 ## Interception decisions
 
-Prompt and post-tool decisions use the same identified `UserMessage` shape as durable user-role input. Each `additionalContexts` entry becomes a separate `user/message`, preserving its identity and provenance. Hook bridges map their native decision fields onto these typed results.
+Pre-step decisions use the same identified `UserMessage` shape as durable user-role input. The entered batch is authoritative and preserves every message's identity and provenance. Hook bridges map their native decision fields onto this typed result.
 
 Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
 
-`agent/prompt-submit` returns a `PromptDecision` before a turn opens. Allow may rewrite the claimed prompt or attach `additionalContexts`; block rejects admission without creating turn events:
+`agent/pre-step` receives one payload carrying the exclusive claimed batch (`messages`), the proposed step's coordinates (`turn`, `step`), and the current turn's cancellation `signal`. The initial proposal runs inside an open turn before any step; a tool continuation may submit an empty claimed batch between steps:
+
+It returns a `PreStepDecision`. Reject opens no step. Enter supplies the complete message batch appended after `step/start`; claimed messages omitted by the final decision remain removed, while input inserted after the claim stays pending:
 
 ```ts type-equiv
-/**
- * Prompt interception result. `allow.content` replaces the prompt, while
- * `additionalContexts` appends model-facing context before the turn starts.
- * An `allow` returned by a listener is authoritative: a listener wrapping
- * `next()` preserves both fields unless it intentionally replaces them.
- */
-type PromptDecision =
-  | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: UserMessage[] }
-  | { kind: 'block'; reason: string }
+/** Whether and with which messages the loop enters a proposed step. */
+type PreStepDecision =
+  | { kind: 'reject' }
+  | { kind: 'enter'; messages: UserMessage[] }
 ```
 
 `agent/request-error` runs after a failed model step closes and before its turn closes. Listeners can repair durable state or await policy work while the failed turn's signal is still live. A handling listener returns `{ kind: 'retry' }` without calling `next()`; the default `undefined` leaves the failure terminal.
@@ -397,12 +232,7 @@ type PromptDecision =
 type RequestErrorAction = { kind: 'retry' } | undefined
 ```
 
-```ts type-equiv
-/** Model-request failure with an optional machine-routable provider code. */
-type RequestError = Error & { code?: string }
-```
-
-`agent/step` is the single serial boundary before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain.
+`agent/pre-step` is the single serial boundary before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain.
 
 `agent/session-start` carries a `SessionStartSource` (why the session lifecycle began; a bridge keys its SessionStart matcher on it):
 
@@ -411,12 +241,71 @@ type RequestError = Error & { code?: string }
 type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
 ```
 
+## Sessions
+
+A `Session` is an **append-only log** of typed `SessionEvent`s — the single source of truth. The LLM message history is *derived* from the log (`deriveMessages()`), not stored separately. Every entry carries a monotonic `seq`, a `time`, and a `type`-discriminated `data` payload; surface variants additionally carry `sourceEventSeqs` provenance and a `surfaceOp`.
+
+The `SessionEvent` envelope's exact conditional shape, the twelve event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/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 execution-enclosure and standalone-event rules 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)**.
+
 ## `ToolDefinition`
 
 The one pipeline-authoring type that is core: what every registered tool *is* — a model-facing `ToolSchema` plus an `execute` function and optional final-content and UI callbacks. A tool author rarely constructs it by hand (the `defineTool` DSL builds it with typed args), but it is the contract the registry holds and the loop dispatches through.
 
 Its full fields, the `defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` typed schema DSL, the `ToolExecution`/`ToolExecutionResult` waterfall shapes, and the tool-presentation UI vocabulary are on **[tools.md](tools.md)**.
 
+## Repo-wide type patterns
+
+Two patterns recur across every subsystem and are documented once, here.
+
+### The `…Map → derived-union` pattern
+
+Almost every extensible sum type in the harness follows one shape: an interface keyed by a discriminant tag (the `…Map`), from which the union is derived with `keyof`. Plugins add variants by **declaration merging** — no edit to the owning 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'; /* … */ }
+  }
+}
+```
+
+Six canonical maps use this pattern; a plugin author extends these:
+
+| Map | Package | Derives | Catalog |
+|---|---|---|---|
+| `ContentBlockMap` | dsh-llm | `ContentBlock` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) |
+| `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) |
+| `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.md#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) |
+
+Two large discriminated unions are the ones consumers `switch` over most: **`StreamChunk`** (the streaming protocol) and **`SessionEvent`** (the log entry). Per the repo convention, `switch` on the tag — don't chain `if`s — so each arm narrows and a typo'd tag fails to compile.
+
+### Branded IDs
+
+IDs that cross package boundaries are **branded** — structurally strings, but non-interchangeable at the type level (a `SessionId` cannot be passed where a `CallId` is expected). Construction goes through a per-type factory; comparison, logging, and JSON behave as ordinary strings.
+
+The `Branded<B>` primitive lives in its own type-only package, [dsh-brand](../../packages/util/brand) (no runtime code, no harness-package dependency), so any package can brand the ids it owns without depending on an unrelated capability package.
+
+Source: [`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 }
+```
+
+The two core IDs are `CallId` (correlates a tool call with its result; dsh-llm) and `SessionId` (the shared live agent and durable session identity; dsh-session). Capability packages brand their own ids too, such as `TaskId` in [tasks.md](tasks.md).
+
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 
 <a id="cordis-surface"></a>
@@ -462,7 +351,7 @@ async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandl
 
 Types: [SessionHeader](persistence.md)
 
-Source: [`packages/core/agent-loop/src/index.ts:252`](../../packages/core/agent-loop/src/index.ts)
+Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts)
 
 <a id="ctxagents--agentregistry"></a>
 
@@ -634,35 +523,12 @@ list(): Agent[]
 roots(): Agent[]
 ```
 
-Source: [`packages/core/agent/src/index.ts:242`](../../packages/core/agent/src/index.ts)
+Source: [`packages/core/agent/src/index.ts:253`](../../packages/core/agent/src/index.ts)
 
 <a id="agent-events"></a>
 
 ### `agent/*` events
 
-<a id="agentcancel-requested--emit"></a>
-
-#### `agent/cancel-requested` — emit
-
-Effective broad cancellation was requested, before queued/outbox work is cleared or the active turn is aborted. This observe-only notification cannot veto cancellation; listener failures are contained.
-
-```ts cordis-catalog
-/**
- * Effective broad cancellation was requested, before queued/outbox work
- * is cleared or the active turn is aborted. This observe-only notification
- * cannot veto cancellation; listener failures are contained.
- * @param agent - the agent whose current work is being cancelled.
- * @param cause - the explicit typed cancellation cause.
- * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
- */
-'agent/cancel-requested'(this: Scoped<Agent>, agent: Agent, cause: AgentCancelCause): void
-```
-
-Types: [Scoped](scope.md)
-
-Source: [`packages/core/agent/src/types.ts:353`](../../packages/core/agent/src/types.ts)
-
 <a id="agentcreated--emit"></a>
 
 #### `agent/created` — emit
@@ -676,11 +542,11 @@ A fully configured agent and live session were published. Setup is composition-o
  * Synchronous listener failure vetoes publication, while returned-promise
  * rejection is reported. Detach requested during dispatch waits until every
  * creation listener has observed the stable entry.
- * @param agent - the newly registered agent with its live session and completed setup.
+ * @param payload.agent - the newly registered agent with its live session and completed setup.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/created'(this: Scoped<Agent>, agent: Agent): void
+'agent/created'(this: Scoped<Agent>, payload: { agent: Agent }): void
 ```
 
 Types: [Scoped](scope.md)
@@ -698,11 +564,11 @@ An agent left the registry; AgentLoop emits this after driver quiescence and sco
  * An agent left the registry; AgentLoop emits this after driver quiescence
  * and scoped-registration unwind, but before session detachment. Custom
  * registry users own their driver-ordering contract.
- * @param agent - the exact agent removed from the registry.
+ * @param payload.agent - the exact agent removed from the registry.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/disposed'(this: Scoped<Agent>, agent: Agent): void
+'agent/disposed'(this: Scoped<Agent>, payload: { agent: Agent }): void
 ```
 
 Types: [Scoped](scope.md)
@@ -713,138 +579,111 @@ Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/t
 
 #### `agent/error` — emit
 
-A step or turn errored. The machine reports a failure here (plus the logger) even when the error has no in-turn position for a durable record.
+A step or turn errored. The machine reports a failure here even when the error has no in-turn position for a durable record.
 
 ```ts cordis-catalog
 /**
- * A step or turn errored. The machine reports a failure here (plus the
- * logger) even when the error has no in-turn position for a durable record.
- * @param agent - the agent whose turn errored.
- * @param turn - the turn in which the failure surfaced.
- * @param step - the step at which the failure surfaced.
- * @param error - the failure, verbatim.
+ * A step or turn errored. The machine reports a failure here even when
+ * the error has no in-turn position for a durable record.
+ * @param payload.agent - the agent whose turn errored.
+ * @param payload.turn - the turn in which the failure surfaced.
+ * @param payload.step - the step at which the failure surfaced.
+ * @param payload.error - the failure, verbatim.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: unknown): void
+'agent/error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; error: unknown }): void
 ```
 
 Types: [Scoped](scope.md)
 
 Source: [`packages/core/agent/src/types.ts:289`](../../packages/core/agent/src/types.ts)
 
-<a id="agentinboxdequeue--emit"></a>
+<a id="agentinboxclaimed--emit"></a>
 
-#### `agent/inbox/dequeue` — emit
+#### `agent/inbox/claimed` — 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.
+One message left the inbox inside its open turn. If the proposed step is rejected, the claimed message ends here: it is neither discarded nor re-emitted as a user/message, and the turn closes without a step.
 
 ```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 item - the exact claimed occurrence.
+ * One message left the inbox inside its open turn. If the proposed step
+ * is rejected, the claimed message ends here: it is neither discarded nor
+ * re-emitted as a user/message, and the turn closes without a step.
+ * @param payload.agent - the agent whose inbox changed.
+ * @param payload.message - the claimed message.
+ * @param payload.turn - the owning turn.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/inbox/dequeue'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
+'agent/inbox/claimed'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage; turn: number }): void
 ```
 
-Types: [Scoped](scope.md)
+Types: [Scoped](scope.md) · [UserMessage](session.md)
 
 Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts)
 
-<a id="agentinboxdiscard--emit"></a>
+<a id="agentinboxdiscarded--emit"></a>
 
-#### `agent/inbox/discard` — emit
+#### `agent/inbox/discarded` — emit
 
-Pending inbox items were dropped without delivering them, so every enqueue occurrence receives exactly one terminal `agent/inbox/dequeue` OR `agent/inbox/discard`. `cancel()` without `keepInbox`, including disposal, emits this after `agent/cancel-requested` when applicable and before aborting the active work. Fires once per drop with every dropped item.
+One message was discarded from the live inbox.
 
 ```ts cordis-catalog
 /**
- * Pending inbox items were dropped without delivering them, so every
- * enqueue occurrence receives exactly one terminal `agent/inbox/dequeue` OR
- * `agent/inbox/discard`. `cancel()` without `keepInbox`, including disposal,
- * emits this after `agent/cancel-requested` when applicable and before
- * aborting the active work. Fires once per drop with every dropped item.
- * @param agent - the agent whose inbox items were dropped.
- * @param items - the discarded occurrences in FIFO order (queued then steering); never empty.
+ * One message was discarded from the live inbox.
+ * @param payload.agent - the agent whose inbox changed.
+ * @param payload.message - the discarded message.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/inbox/discard'(this: Scoped<Agent>, agent: Agent, items: InboxItem[]): void
+'agent/inbox/discarded'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
 ```
 
-Types: [Scoped](scope.md)
+Types: [Scoped](scope.md) · [UserMessage](session.md)
 
 Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts)
 
-<a id="agentinboxenqueue--emit"></a>
+<a id="agentinboxinserted--emit"></a>
 
-#### `agent/inbox/enqueue` — emit
+#### `agent/inbox/inserted` — emit
 
-An item entered the queued or steering inbox. `placement` is the acceptance-time routing result; listeners must not reconstruct it from later agent or session state.
+One message entered the live inbox.
 
 ```ts cordis-catalog
 /**
- * An item entered the queued or steering inbox. `placement` is the
- * acceptance-time routing result; listeners must not reconstruct it from
- * later agent or session state.
- * @param agent - the owning agent.
- * @param item - accepted occurrence, message, and resolved placement.
+ * One message entered the live inbox.
+ * @param payload.agent - the agent whose inbox changed.
+ * @param payload.message - the inserted message.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
+'agent/inbox/inserted'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
 ```
 
-Types: [Scoped](scope.md)
+Types: [Scoped](scope.md) · [UserMessage](session.md)
 
 Source: [`packages/core/agent/src/types.ts:185`](../../packages/core/agent/src/types.ts)
 
-<a id="agentinboxupdate--emit"></a>
-
-#### `agent/inbox/update` — emit
-
-A still-pending queued item changed content. The item id, placement, and position remain stable while the event carries the replacement message.
-
-```ts cordis-catalog
-/**
- * A still-pending queued item changed content. The item id, placement, and
- * position remain stable while the event carries the replacement message.
- * @param agent - the owning agent.
- * @param item - the complete post-update occurrence.
- * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
- */
-'agent/inbox/update'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
-```
-
-Types: [Scoped](scope.md)
-
-Source: [`packages/core/agent/src/types.ts:321`](../../packages/core/agent/src/types.ts)
-
-<a id="agentprompt-submit--waterfall"></a>
+<a id="agentpre-step--waterfall"></a>
 
-#### `agent/prompt-submit` — waterfall
+#### `agent/pre-step` — waterfall
 
-Allow, rewrite, or block one claimed prompt before it becomes a user message or opens a turn. Call `next()` for the unchanged default. The signal controls only this admission attempt; listeners may cooperate with it but must not retain it for a later attempt or turn.
+Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages.
 
 ```ts cordis-catalog
 /**
- * Allow, rewrite, or block one claimed prompt before it becomes a user
- * message or opens a turn. Call `next()` for the unchanged default. The
- * signal controls only this admission attempt; listeners may cooperate with
- * it but must not retain it for a later attempt or turn.
- * @param agent - the agent whose turn claimed the message.
- * @param message - the frozen claimed message, including identity and source.
- * @param signal - the current turn's explicit abort signal.
+ * Reject a proposed step or replace the messages that enter it. Calling
+ * `next()` preserves the current messages.
+ * @param payload.agent - the agent proposing the step.
+ * @param payload.messages - messages removed from the inbox for this step.
+ * @param payload.turn - the turn that will own the step.
+ * @param payload.step - the step proposed by the loop.
+ * @param payload.signal - the current turn's cancellation signal.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode waterfall
  */
-'agent/prompt-submit'(this: Scoped<Agent>, agent: Agent, message: UserMessage, signal: AbortSignal, next: () => Promise<PromptDecision>): Promise<PromptDecision>
+'agent/pre-step'(this: Scoped<Agent>, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
 ```
 
 Types: [Scoped](scope.md) · [UserMessage](session.md)
@@ -863,14 +702,14 @@ Replace the frozen call configuration. `await next()` yields the config the mach
  * the machine would use (agent options on the first request, the logged
  * header afterwards); return a replacement to switch. Model-visible
  * content must use logged channels; this seam cannot mutate messages.
- * @param agent - the agent making the model call.
- * @param turn - the open turn number.
- * @param step - the step whose request this is.
- * @param signal - the current turn's explicit abort signal.
+ * @param payload.agent - the agent making the model call.
+ * @param payload.turn - the open turn number.
+ * @param payload.step - the step whose request this is.
+ * @param payload.signal - the current turn's explicit abort signal.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode waterfall
 */
-'agent/request'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
+'agent/request'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
 ```
 
 Types: [LlmCallConfig](llm-streaming.md) · [Scoped](scope.md)
@@ -881,28 +720,25 @@ Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/t
 
 #### `agent/request-error` — waterfall
 
-Handle a model-request failure after its failed step has closed but before the failed turn closes. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns the error, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.
+Handle one failed model-request attempt before the loop retries or closes its step. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns recovery, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.
 
 ```ts cordis-catalog
 /**
- * Handle a model-request failure after its failed step has closed but
- * before the failed turn closes. A listener returns `{ kind: 'retry' }`
- * without calling `next()` when it owns the error, or calls `next()` to
- * delegate. The default `undefined` leaves the failure terminal.
- * @param agent - the agent whose request failed.
- * @param turn - the open turn number.
- * @param step - the failed step number.
- * @param error - the original model-request failure.
- * @param failure - serializable facts normalized at the final adapter boundary.
- * @param priorFailures - immutable failures that already authorized another
- * retry turn in this consecutive sequence.
- * @param retryPolicy - immutable policy of the adapter registration that served
- * the failed request, or `undefined` if no final adapter served it.
- * @param signal - the turn abort signal.
+ * Handle one failed model-request attempt before the loop retries or closes
+ * its step. A listener returns `{ kind: 'retry' }` without calling `next()`
+ * when it owns recovery, or calls `next()` to delegate. The default
+ * `undefined` leaves the failure terminal.
+ * @param payload.agent - the agent whose request failed.
+ * @param payload.turn - the turn containing the failed request.
+ * @param payload.step - the step containing the failed request attempt.
+ * @param payload.provider - the provider selected for the failed request.
+ * @param payload.failure - serializable facts normalized at the final adapter boundary.
+ * @param payload.retryPolicy - the policy of the adapter registration that served the failed request.
+ * @param payload.signal - the turn abort signal.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode waterfall
  */
-'agent/request-error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, priorFailures: readonly LlmFailure[], retryPolicy: ResolvedRetryPolicy | undefined, signal: AbortSignal, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
+'agent/request-error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; provider: string; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined; signal: AbortSignal }, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
 ```
 
 Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md) · [Scoped](scope.md)
@@ -921,12 +757,12 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
  * `agent.inject()` to seed model-facing context. This is a notification, not
  * a veto; disposal requested by a lifecycle owner is rechecked before the
  * driver starts.
- * @param agent - the agent whose session lifecycle began.
- * @param source - why the session started (fresh startup, resume, …).
+ * @param payload.agent - the agent whose session lifecycle began.
+ * @param payload.source - why the session started (fresh startup, resume, …).
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/session-start'(this: Scoped<Agent>, agent: Agent, source: SessionStartSource): void
+'agent/session-start'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource }): void
 ```
 
 Types: [Scoped](scope.md)
@@ -937,18 +773,19 @@ Source: [`packages/core/agent/src/types.ts:216`](../../packages/core/agent/src/t
 
 #### `agent/status` — emit
 
-Agent status changed (`idle` ⇄ `running`). `send()` does not enter `running` synchronously; drive lifecycle from this event.
+Agent status changed (`idle` ⇄ `running`). A waking delivery enters `running` synchronously after reserving cancellation; `idle` means no driver remains scheduled or active.
 
 ```ts cordis-catalog
 /**
- * Agent status changed (`idle` ⇄ `running`). `send()` 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).
+ * Agent status changed (`idle` ⇄ `running`). A waking delivery enters
+ * `running` synchronously after reserving cancellation; `idle` means no
+ * driver remains scheduled or active.
+ * @param payload.agent - the agent whose status flipped.
+ * @param payload.status - the status just entered (the transition's destination).
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/status'(this: Scoped<Agent>, agent: Agent, status: AgentStatus): void
+'agent/status'(this: Scoped<Agent>, payload: { agent: Agent; status: AgentStatus }): void
 ```
 
 Types: [Scoped](scope.md)
@@ -959,7 +796,7 @@ Source: [`packages/core/agent/src/types.ts:177`](../../packages/core/agent/src/t
 
 #### `agent/turn-stopping` — serial
 
-The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (`agent.steer(...)`) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying `concludesTurn` ends the turn at its step.
+The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (`agent.steer(...)`) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying `concludesTurn` ends the turn at its step. The conclusion never short-circuits already-submitted next-step work: same-step `additionalContexts` or racing steering still runs, and the turn closes only when that inbox drains.
 
 ```ts cordis-catalog
 /**
@@ -969,14 +806,17 @@ The turn is about to close: the model owes no response (no live tool calls, no f
  * re-reads its inbox: fresh steering runs another step, none closes the
  * turn. Data decides, so listener order cannot change the outcome. The
  * inverse control (stop a tool loop early) is data too: a tool result
- * carrying `concludesTurn` ends the turn at its step.
- * @param agent - the agent whose turn is at its stop boundary.
- * @param turn - the turn about to close.
- * @param signal - the current turn's explicit abort signal.
+ * carrying `concludesTurn` ends the turn at its step. The conclusion
+ * never short-circuits already-submitted next-step work: same-step
+ * `additionalContexts` or racing steering still runs, and the turn
+ * closes only when that inbox drains.
+ * @param payload.agent - the agent whose turn is at its stop boundary.
+ * @param payload.turn - the turn about to close.
+ * @param payload.signal - the current turn's explicit abort signal.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode serial
  */
-'agent/turn-stopping'(this: Scoped<Agent>, agent: Agent, turn: number, signal: AbortSignal): Promise<void> | void
+'agent/turn-stopping'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; signal: AbortSignal }): Promise<void> | void
 ```
 
 Types: [Scoped](scope.md)
@@ -999,12 +839,12 @@ A declarative agent entry failed before it could publish a live agent. Consumers
  * Consumers that buffer work for the configured identity use this
  * transient signal to reject that work instead of waiting forever. Normal
  * factory teardown suppresses failures from the cancelled startup attempt.
- * @param sessionId - exact shared agent/session identity that failed startup.
- * @param error - persistence, setup, or publication failure.
+ * @param payload.sessionId - exact shared agent/session identity that failed startup.
+ * @param payload.error - persistence, setup, or publication failure.
  * @mode emit
  */
-'agent-loop/config-start-failed'(sessionId: SessionId, error: unknown): void
+'agent-loop/config-start-failed'(payload: { sessionId: SessionId; error: unknown }): void
 ```
 
-Source: [`packages/core/agent-loop/src/index.ts:157`](../../packages/core/agent-loop/src/index.ts)
+Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 272 - 428
docs/subsystems/core.zh.md

@@ -2,11 +2,11 @@
 
 [English](core.md) | 中文
 
-**核心**子系统即 [`packages/core`](../../packages/core/README.md)——每个组合都会启动的控制主干:事件溯源的会话日志、系统提示词组装、工具注册表、agent 词汇,以及驱动它们的具体循环。本页拥有主干的共享词汇——`Agent` 句柄及其投递与拦截契约,以及全仓通用的类型模式——并指引到该组的专属页面;目录索引见[子系统 README](README.md)。
+**核心**子系统即 [`packages/core`](../../packages/core/README.md)——每个组合都会启动的控制主干:事件溯源的会话日志、系统提示词组装、工具注册表、agent 词汇,以及驱动它们的具体循环。本页拥有 `agent`/`agent-loop` 这对包所声明的内容——agent 如何被创建与拥有,以及 `Agent` 句柄及其投递、取消与拦截契约——外加每个子系统都遵循的两个类型模式;该组的专属页面与目录其余部分见[子系统 README](README.md)。
 
 ## 主干逐包速览
 
-一个轮次按同一条循环流经六个包:[`agent-loop`](../../packages/core/agent-loop) 中的 driver 认领一条排队的 prompt,在[会话日志](session.md)(`ctx.sessions`)上开启轮次,通过 [system-prompt](system-prompt.md)(`ctx.systemPrompt`)组装请求前缀并从日志派生历史,经 [LLM seam](llm-streaming.md) 流式获取模型响应,经[工具注册表](tools.md)(`ctx.tools`)分发工具调用,并把每个模型可见的事实追加回日志,供下一步派生。
+一个轮次按同一条循环流经六个包:[`agent-loop`](../../packages/core/agent-loop) 中的 driver 认领一条排队的 prompt,在[会话日志](session.md)(`ctx.sessions`)上开启轮次,通过 [system-prompt](system-prompt.md)(`ctx.systemPrompt`)组装请求前缀并从日志派生历史,经 [LLM seam](llm-streaming.md) 流式获取模型响应,经[工具注册表](tools.md)(`ctx.tools`)分发工具调用,并把每个模型可见的事实追加回日志,供下一步派生。循环搬运的对话词汇——`Message`、`ContentBlock`、`StreamChunk`、模型请求——由 [`packages/llm`](../../packages/llm/README.md) 声明,记录在 [llm-streaming.md](llm-streaming.md)。
 
 | 包 | 负责内容 | 页面 |
 |---|---|---|
@@ -19,212 +19,49 @@
 
 `scope/` 是这里唯一的非服务包:一个零依赖库(`createScope`/`scopeOf`/`scopeTarget`),在模块图中位于 `session/` 与 `system-prompt/` 之下,正是为了让它们消费它而不形成环。`agent-loop` 是 `agent` seam 的唯一具体实现,放在这里因为它是 harness 的默认产品循环;它在 `ctx.agents.withInitiator()` 内运行每个 driver。扩展插件依赖 `agent`——包括需要发起 Agent 时——而绝不直接依赖 `agent-loop`,因此循环保持可替换。把这条主干接成可运行 agent 的默认组合是 [`examples/agent-spine-demo`](../../packages/examples/agent-spine-demo/README.md)。
 
-<a id="what-counts-as-core"></a>
+<a id="creation-and-ownership"></a>
 
-## 本页拥有什么
+## 创建与所有权
 
-循环搬运的对话词汇——`Message`、`ContentBlock`、`StreamChunk`、模型请求——由 [`packages/llm`](../../packages/llm/README.md) 声明,记录在 [llm-streaming.md](llm-streaming.md);会话事件、提示词组装与工具词汇在上表该组的专属页面。留在本页的是所有部分共享的词汇:`Agent` 句柄及其投递、取消与拦截契约(由 `packages/core/agent` 声明)、`SessionEvent` 信封,以及每个子系统都遵循的两个类型模式。范围界定规则记录在[子系统目录 Agent Note](../../.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md):你编写、持有或接收的类型记录在其声明子系统所在之处;为它提供类型推导、渲染或持久化的机制留在该机制自己的页面。
+消费方通过 `ctx.agents` 创建 agent——`create()` 在一个调用方提供的 `SessionId` 下构建全新会话与 agent,`resume()` 先加载持久会话——或者通过循环的声明式配置条目创建。编程式创建返回归属所有者的句柄:
 
-## `…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` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) |
-| `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) |
-| `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.md#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`。
-
-## 会话
-
-`Session` 是一份类型化 `SessionEvent` 的**仅追加日志**——唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`),而非单独存储。事件词汇从 `SessionEventMap` 派生:
-
-源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
+源码:[`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index.ts)
 
 ```ts type-equiv
 /**
- * One immutable entry in the session log.
+ * An owned agent plus its disposer, returned by {@link AgentRegistry.create} /
+ * {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers,
+ * only the holder can tear this agent down. The registered factory provider is
+ * also a structural owner because the scoped agent depends on that provider's
+ * service surface; provider unload stops and drains every live handle it made.
+ * `dispose()` stops the loop, awaits its exit, unregisters the agent, removes
+ * its session from the store, and finally unwinds its scoped world.
  *
- * 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]
+ * `ctx.agents.get(id)` still returns a bare {@link Agent} — the handle is
+ * exposed only to the consumer owner that created it; the structural provider
+ * reaches the same teardown internally. Config-created agents (the loop's own
+ * startup) are owned by the loop fiber and never need a handle.
+ */
+interface AgentHandle {
+  agent: Agent
+  dispose(): Promise<void>
+}
 ```
 
-十二种事件变体(`turn/start`、`turn/end`、`step/start`、`step/end`、`user/message`、`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)** 中。
+`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、seed 边界、来源分类、委派深度)、fork 用的可选 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应物:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在两个 id 都尚未发布时组装 agent 的作用域世界——凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在——并可返回一个在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose 都会回滚事务,两个 id 均不发布。
+
+`AgentFactory` 是注册表背后的创建 seam:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方面向 `ctx.agents` 编程,无需依赖具体循环包。确切的 `create`/`resume` 签名及其回滚契约见下方[生成区块](#ctxagents--agentregistry)。
 
 <a id="the-agent-handle"></a>
 
 ## Agent 句柄
 
-`Agent` 是每个插件(UI、钩子、orchestrator)面向编程的 surface。具体实现为 dsh-agent-loop 包内部细节;循环外没有任何组件依赖它。
+`Agent` 是每个插件(UI、钩子、orchestrator)面向编程的 surface;`ctx.agents.get(id)` 返回它,[发起者作用域](#initiating-agent)携带它。具体实现为 dsh-agent-loop 包内部细节;循环外没有任何组件依赖它。统一的 `send` 方法直接暴露 target 与 wakeup 路由;`followup`、`steer` 与 `inject` 是固定预设的别名方法。
 
 源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
 
 ```ts type-equiv
-/**
- * Which inbox queue a {@link Agent.send} item joins:
- * - `next-turn` — the item becomes its own turn, claimed at a turn boundary.
- * - `next-step` — during prompt admission or an open turn, the item stages for
- *   the next safe step boundary; otherwise it is promoted per its `wakeup`
- *   flag.
- */
-type SendTarget = 'next-turn' | 'next-step'
-```
-
-```ts type-equiv
-/** Resolved inbox placement reported when an accepted message is enqueued. */
-type InboxPlacement = 'queued' | 'steering'
-```
-
-`InboxItemId` 是为每次获准进入 FIFO 的项铸造的进程本地品牌字符串。它有意区别于 `MessageId`:同一条不可变消息发送两次,会创建两个可独立寻址的待处理项。
-
-```ts type-equiv
-/** One independently addressable accepted occurrence in an agent inbox. */
-interface InboxItem {
-  /** Agent-loop-minted occurrence identity. */
-  readonly id: InboxItemId
-  /** Identified message delivered by the caller. */
-  readonly message: UserMessage
-  /** Acceptance-time FIFO classification. */
-  readonly placement: InboxPlacement
-}
-```
-
-```ts type-equiv
-/** A user-requested mutation of one still-pending queued occurrence. */
-type InboxAction =
-  | { readonly kind: 'edit'; readonly content: ContentBlock[] }
-  | { readonly kind: 'remove' }
-  | { readonly kind: 'steer' }
-```
-
-```ts type-equiv
-/** Result of applying an inbox action at the synchronous ownership boundary. */
-type InboxActionResult = 'applied' | 'not-found' | 'steer-unavailable'
-```
-
-```ts type-equiv
-/**
- * Options for the unified {@link Agent.send} primitive over the
- * (`target` × `wakeup`) matrix. Named presets: {@link Agent.followup}
- * (`next-turn`/wakeup), {@link Agent.steer} (`next-step`/wakeup), and
- * {@link Agent.inject} (`next-step`/no-wakeup).
- *
- * The object is complete so routing policy is explicit.
- */
-interface SendOptions {
-  /** Queue the item joins. */
-  target: SendTarget
-  /**
-   * Whether this item makes the model run: wake a parked driver (`next-turn`)
-   * or force a continuation step (`next-step` while running). A `false`
-   * `next-turn` item queues without waking; a `false`
-   * `next-step` item attaches durable context without forcing another step
-   * (the injection preset).
-   */
-  wakeup: boolean
-}
-```
-
-固定预设的别名方法自带 `target` 与 `wakeup`;其已有标识的 `UserMessage` 会携带角色、内容与 provenance。编辑替换内容或严格 steering(中途引导)转移不可变消息时,其 `MessageId` 都保持稳定。原 queued 单次入队项会结束,严格 steering 则接受一个具有不同 `InboxItemId` 的新 steering 单次入队项。注入绕过两个 FIFO,从不出现在 inbox 生命周期事件中。
-
-```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
-}
-```
-
-`SteeringReceipt.outcome` 始终会解析。`admitted` 标识其不可变请求历史包含该确切消息的轮次与步骤;`rejected` 表示生命周期或终止策略先丢弃了该消息。同步输入校验仍会从 `steer()` 抛出异常。
-
-```ts type-equiv
-/** Stable runtime cause accepted by {@link Agent.cancel}. */
-type AgentCancelCause =
-  | { readonly kind: 'user' }
-  | { readonly kind: 'parent' }
-```
-
-`Agent` 是覆盖公开活跃 agent 契约的接口。具体驱动器拥有 `followup`/`steer`/`inject` 别名方法,并将它们经由 `send` 的(`target` × `wakeup`)矩阵路由。
-
-```ts type-equiv
-/**
- * Public live-agent handle with aliases over the unified delivery primitive.
- * @typert object
- */
+/** Public live-agent handle. */
 interface Agent {
   /** The single identity shared with {@link session}. */
   readonly id: SessionId
@@ -232,78 +69,28 @@ interface Agent {
   readonly options: AgentOptions
   /** The live session this agent drives; its log is the durable source of truth. */
   readonly session: Session
+  /** The agent-owned projection of durable pending work. */
+  readonly inbox: Inbox
   /** The current lifecycle state, mirrored on every `agent/status` transition. */
   readonly status: AgentStatus
-  /**
-   * Whether a `next-step` send currently stages for prompt admission or the
-   * open turn. Unlike {@link status}, this excludes admission exit and turn
-   * settlement, when a waking `next-step` send becomes a queued follow-up.
-   */
-  readonly acceptsNextStep: boolean
   /** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
   readonly ctx: Context
 
-  /**
-   * The unified delivery primitive over the (`target` × `wakeup`) matrix.
-   * It routes the caller's typed content and source as follows:
-   *
-   * - `next-turn` queues an item that becomes the sole ordinary message of its
-   *   own FIFO-ordered turn; `wakeup:true` wakes a
-   *   parked driver, while `wakeup:false` queues without waking.
-   * - `next-step` with `wakeup:true` stages steering during prompt admission
-   *   or an open turn; outside that window it falls back to a woken
-   *   `next-turn`.
-   * - `next-step` with `wakeup:false` injects durable model-facing context
-   *   without running the model: admission or an open turn stages it for the
-   *   next safe log position, while an injection outside that window appends
-   *   immediately without opening a turn. If admission closes without a turn,
-   *   a context-only boundary appends immediately; context staged beside
-   *   steering remains pending with it.
-   * The agent publishes or queues the identified frozen message as-is.
-   * @param message - identified model-facing content and its producer provenance.
-   * @param options - target queue and wakeup decision.
-   */
-  send(message: UserMessage, options: SendOptions): void
-
-  /**
-   * Reserve admission of the next ordinary turn while this agent is idle, so an
-   * operation can mutate durable history before any queued prompt derives a
-   * request from it. Already-accepted waking work has right of way, including a
-   * send whose wake is still a pending microtask. Later sends keep their
-   * ordinary placement, FIFO order, and `wakeup` facts, and
-   * {@link acceptsNextStep} stays `false`, so a waking `next-step` send becomes
-   * a queued follow-up rather than steering; cancellation and disposal may
-   * still discard them. {@link inject} is not withheld. {@link whenIdle} treats
-   * a live reservation as activity, while lifecycle teardown does not await it.
-   * @returns the idempotent release, or `undefined` when the agent is running, already reserved, or already committed to waking work.
-   */
-  reserveTurnAdmission(): (() => void) | undefined
-
-  /**
-   * Mutate one still-pending queued occurrence synchronously. Editing preserves
-   * the message identity and queue position; removal publishes its terminal
-   * discard. Steer strictly transfers the message into the current next-step
-   * window, or returns `steer-unavailable` without changing the queued
-   * occurrence. Steering occurrences and driver-claimed items return
-   * `not-found`.
-   * @param id - independently addressable queued occurrence.
-   * @param action - edit, remove, or strict steer operation.
-   * @returns the applied outcome or the reason no mutation occurred.
-   */
-  updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult
-
   /**
    * 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. Idle
-   * cancellation is a no-op and does not arm later work.
-   * @param cause - the stable caller intent carried by the current turn signal.
+   * turn or between-turn task. The first cause wins for that activity. With no
+   * active activity, cancellation is a no-op and does not arm later work.
+   * @param cause - the stable caller intent carried by the active operation 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. */
+  /**
+   * Resolve after the current whole-agent activity reaches quiescence. This
+   * follows replacement work started before the observed driver retires,
+   * but does not identify the settlement of any particular message.
+   * @returns fulfillment after no active driver or maintenance task remains.
+   */
   whenIdle(): Promise<void>
 
   /**
@@ -338,60 +125,110 @@ interface Agent {
   followup(message: UserMessage): void
 
   /**
-   * Submit steering with a message-owned admission receipt — the
-   * `next-step`/wakeup preset of {@link send}. During prompt admission or an
-   * open turn, the message waits in the steering FIFO until a committed step
-   * snapshots it; outside that window it enters the ordinary queued FIFO. The
-   * receipt resolves `admitted` only after the message joins that step's
-   * immutable request history, or `rejected` when terminal policy,
-   * cancellation, or disposal discards it first. A non-terminal turn close may
-   * leave it staged for a later admitted prompt without settling the receipt.
+   * Submit steering for the nearest step. An idle driver starts a turn;
+   * a running driver consumes it at its next step boundary.
+   * A rejected step leaves steering parked in the inbox until the next
+   * wake; cancellation or disposal may discard pending steering.
    * @param message - identified steering content and its producer provenance.
-   * @returns the receipt for this exact message's eventual admission outcome.
    */
-  steer(message: UserMessage): SteeringReceipt
+  steer(message: UserMessage): void
 
   /**
-   * Append model-facing context without running the model — the
-   * `next-step`/no-wakeup preset of {@link send}. Admission or an open turn
-   * stages it at the next safe log position; outside that window it appends
-   * immediately without opening a turn. If admission closes without a turn,
-   * a context-only boundary appends immediately; context staged beside
-   * steering remains pending with it.
+   * Queue model-facing context for the next pre-step without waking the
+   * driver. A running driver claims it at the nearest later step boundary;
+   * idle drivers leave it pending until follow-up or steering
+   * wakes them. It may miss a request whose pre-step already claimed its
+   * batch. Cancellation or disposal may discard pending context.
    * @param message - identified injected context and its producer provenance.
    */
   inject(message: UserMessage): void
 }
 ```
 
-`AgentStatus` 为 `'idle' | 'running'`,`SessionId` 是品牌类型。dispose(资源释放)会把 agent 从注册表移除并发出 `agent/disposed`;它不是一个终态 status 值。`running` 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。对于需要在把输入作为 steering 加入当前提示词准入/轮次,还是提交为一个新的待准入提示词之间做选择的调用方,`acceptsNextStep` 才是更窄且准确的路由判断条件。活动的轮次接纳预留与完全停稳相关,但不会改变 `status`,也不会把之后的队列项变成 steering;它的唯一权限是将驱动器的下一次认领延迟到释放时。`AgentOptions` 可合并扩展:core 声明 `provider?`、`model?` 与 `maxTokens?`(在 `agent/request` 后,分发要求 provider 与 model 都存在)。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。Persona 归 `dsh-system-prompt` 所有:agent 作用域的 `deployment:persona` 可以遮蔽全局默认值。
+```ts type-equiv
+/**
+ * An agent's lifecycle state, emitted on every transition as `agent/status`:
+ * `idle` means no driver is active; `running` begins when waking input starts
+ * cancellable pre-step processing and lasts while the driver drains,
+ * closes, or checkpoints turns. Disposal removes the agent from its registry;
+ * it is not a third observable status.
+ */
+type AgentStatus = 'idle' | 'running'
+```
+
+`running` 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。dispose(资源释放)会把 agent 从注册表移除并发出 `agent/disposed`;它不是一个终态 status 值。`followup()` 不返回句柄:其 `MessageId` 标识的是持久的 inbox 插入、认领与丢弃事实,而非之后的助手输出或轮次结束。`whenIdle()` 观察的是整个 agent,因此只有当调用方明确拥有从回执到空闲的这段区间时,才能把它称为一次 run([决策](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md))。
+
+```ts type-equiv
+/** Merge-extensible agent creation options. Persona belongs to system-prompt sections. */
+interface AgentOptions {
+  /** Provider route (must have a registered adapter at call time). */
+  provider?: string
+  /** Model id interpreted by the selected provider adapter. */
+  model?: string
+  /** Maximum output tokens for each conversation-model request. */
+  maxTokens?: number
+}
+```
+
+在 `agent/request` 之后,分发要求 `provider` 与 `model` 都存在。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。agent 作用域的 `deployment:persona` 提示词段落可以遮蔽全局默认 persona。
+
+inbox 即投递词汇——agent 以持久投影形式拥有的两条有序待处理消息列表:
+
+```ts type-equiv
+/** One of the two ordered pending-message lists owned by an agent. */
+type InboxTarget = 'next-turn' | 'next-step'
+```
+
+每个待处理入队项就是其 `UserMessage`;`MessageId` 是唯一标识。`Inbox.append`、`prepend`、`replace`、`remove`、`clear`、`splice` 与 `claim` 会记录规范化的持久 `agent/inbox/spliced` 变更,并拒绝重复的待处理 id。`replace(messageId, newMessage)` 与 `remove(messageId)` 通过 `MessageId` 跨两份列表定位待处理消息;替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。普通删除和 `clear()` 都表示取消。`claim(target)` 通过无 outcome 的纯删除 splice 移除拟进入步骤的批次——全部 `next-step` 输入,外加轮次边界上的一条 `next-turn` 消息——且不发出 discarded 通知;循环另行逐条发出 claimed 通知。UI 投影等整体队列消费方通过持久 splice 重建 `nextTurn` 与 `nextStep`,而跟踪单条消息的消费方使用精确的 `agent/inbox/inserted`、`claimed` 与 `discarded` 通知。
 
-cause 是由 TypeScript 强制约束的同进程输入。活跃的 `TurnCancellation` 持有者会把其判别字段复制到仅运行时的 `AbortSignal.reason`,并在发布 `turn/end` 前退役;冻结后的 `AbortSignal.reason` 仍可读取。只有 loop 会在结算时从自己机器私有的 signal 上读回 cause(`user`、`parent` 或仅用于生命周期的 `disposed`)——不存在公开的读取器,signal 也不授予协作监听器任何分类权限。持久 `turn/end` 保留粗粒度 `{ kind: 'aborted' }` 结果;若需记录请求 provenance,应使用单独的持久事件,而不是让终态结果承担额外含义。
+取消:
+
+```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 canceled inbox splice is logged.
+   */
+  keepInbox?: boolean | undefined
+}
+```
+
+```ts type-equiv
+/** Why an active agent driver was cancelled. */
+type AgentCancelCause =
+  | { readonly kind: 'user' }
+  | { readonly kind: 'parent' }
+  | { readonly kind: 'hook'; readonly reason: string }
+  | { readonly kind: 'disposed' }
+```
+
+cause 是由 TypeScript 强制约束的同进程输入。活跃的取消持有者会将它复制到仅运行时的 `AbortSignal.reason`;signal 不授予协作监听器任何分类权限。持久 `turn/end` 保留粗粒度 `{ kind: 'aborted' }` 结果;若需记录请求 provenance,应使用单独的持久事件,而不是让终态结果承担额外含义。
 
 [事件分类](../architecture.md#event)拥有 `agent/*` 生命周期、检查点与 waterfall(瀑布式事件)契约。轮次和步骤边界是持久会话事件,而不是 agent emit。
 
+<a id="initiating-agent"></a>
+
 ## 发起 Agent
 
 `ctx.agents` 携带的进程本地 initiator 就是上面的确切 `Agent`,不是单独的 frame 或复制的标识。环境中存在该值既不能证明存活,也不代表授权;其生命周期与边界规则由 [initiator 作用域决策](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)规定。
 
 ## 拦截决策
 
-提示词决策与工具后决策使用与持久 user-role 输入相同、带标识的 `UserMessage` 形状。每个 `additionalContexts` 条目都会成为一条独立的 `user/message`,保留各自的标识与 provenance。钩子桥接层把其原生决策字段映射到这些类型化结果上。
+pre-step 决策使用与持久 user-role 输入相同、带标识的 `UserMessage` 形状。进入步骤的批次具有权威性,并保留每条消息的标识与 provenance。钩子桥接层把其原生决策字段映射到这一类型化结果上。
 
 源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
 
-`agent/prompt-submit` 在轮次打开前返回 `PromptDecision`。allow 可以改写已领取的提示词或附加 `additionalContexts`;block 拒绝准入且不产生任何轮次事件:
+`agent/pre-step` 接收一个 payload,携带独占的已领取批次(`messages`)、拟进入步骤的坐标(`turn`、`step`)与当前轮次的取消 `signal`。首次提案在已打开的轮次内、任何步骤开始前运行;工具 continuation 可以在步骤之间提交空的已领取批次:
+
+它返回 `PreStepDecision`。reject 不会打开步骤。enter 提供在 `step/start` 后追加的完整消息批次;最终决策省略的已领取消息保持已删除,而领取后插入的输入仍留待后续处理:
 
 ```ts type-equiv
-/**
- * Prompt interception result. `allow.content` replaces the prompt, while
- * `additionalContexts` appends model-facing context before the turn starts.
- * An `allow` returned by a listener is authoritative: a listener wrapping
- * `next()` preserves both fields unless it intentionally replaces them.
- */
-type PromptDecision =
-  | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: UserMessage[] }
-  | { kind: 'block'; reason: string }
+/** Whether and with which messages the loop enters a proposed step. */
+type PreStepDecision =
+  | { kind: 'reject' }
+  | { kind: 'enter'; messages: UserMessage[] }
 ```
 
 `agent/request-error` 在失败的模型步骤关闭之后、其轮次关闭之前运行。listener 可以在失败轮次的 signal 仍然存活时修复持久状态或 await 策略工作。处理该错误的 listener 返回 `{ kind: 'retry' }` 且不调用 `next()`;默认的 `undefined` 会让失败保持终态。
@@ -401,12 +238,7 @@ type PromptDecision =
 type RequestErrorAction = { kind: 'retry' } | undefined
 ```
 
-```ts type-equiv
-/** Model-request failure with an optional machine-routable provider code. */
-type RequestError = Error & { code?: string }
-```
-
-`agent/step` 是请求推导前唯一的串行边界。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。
+`agent/pre-step` 是请求推导前唯一的串行边界。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。
 
 `agent/session-start` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart):
 
@@ -415,12 +247,73 @@ type RequestError = Error & { code?: string }
 type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
 ```
 
+## 会话
+
+`Session` 是一份类型化 `SessionEvent` 的**仅追加日志**——唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`),而非单独存储。每个条目携带单调的 `seq`、`time` 与按 `type` 判别的 `data` payload;surface 变体还额外携带 `sourceEventSeqs` provenance 与 `surfaceOp`。
+
+`SessionEvent` 信封的确切条件形状、十二种事件变体(`turn/start`、`turn/end`、`step/start`、`step/end`、`user/message`、`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)** 中。
+
 ## `ToolDefinition`
 
 唯一属于核心的流水线编写类型:每个已注册工具*是什么*——一个面向模型的 `ToolSchema` 加上一个 `execute` 函数,以及可选的最终内容回调与 UI 回调。工具作者很少手动构造它(`defineTool` DSL 会用类型化参数构建),但它是注册表持有、循环分发所经过的契约。
 
 其完整字段、`defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` 类型化 schema DSL、`ToolExecution`/`ToolExecutionResult` waterfall 形状,以及工具展示 UI 词汇在 **[tools.md](tools.md)** 中。
 
+## 全仓通用类型模式
+
+两个模式在每个子系统中反复出现,只在此处记录一次。
+
+### `…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` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) |
+| `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) |
+| `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.md#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`。
+
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 
 <a id="cordis-surface"></a>
@@ -466,7 +359,7 @@ async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandl
 
 Types: [SessionHeader](persistence.md)
 
-Source: [`packages/core/agent-loop/src/index.ts:252`](../../packages/core/agent-loop/src/index.ts)
+Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts)
 
 <a id="ctxagents--agentregistry"></a>
 
@@ -638,35 +531,12 @@ list(): Agent[]
 roots(): Agent[]
 ```
 
-Source: [`packages/core/agent/src/index.ts:242`](../../packages/core/agent/src/index.ts)
+Source: [`packages/core/agent/src/index.ts:253`](../../packages/core/agent/src/index.ts)
 
 <a id="agent-events"></a>
 
 ### `agent/*` events
 
-<a id="agentcancel-requested--emit"></a>
-
-#### `agent/cancel-requested` — emit
-
-Effective broad cancellation was requested, before queued/outbox work is cleared or the active turn is aborted. This observe-only notification cannot veto cancellation; listener failures are contained.
-
-```ts cordis-catalog
-/**
- * Effective broad cancellation was requested, before queued/outbox work
- * is cleared or the active turn is aborted. This observe-only notification
- * cannot veto cancellation; listener failures are contained.
- * @param agent - the agent whose current work is being cancelled.
- * @param cause - the explicit typed cancellation cause.
- * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
- */
-'agent/cancel-requested'(this: Scoped<Agent>, agent: Agent, cause: AgentCancelCause): void
-```
-
-Types: [Scoped](scope.md)
-
-Source: [`packages/core/agent/src/types.ts:353`](../../packages/core/agent/src/types.ts)
-
 <a id="agentcreated--emit"></a>
 
 #### `agent/created` — emit
@@ -680,11 +550,11 @@ A fully configured agent and live session were published. Setup is composition-o
  * Synchronous listener failure vetoes publication, while returned-promise
  * rejection is reported. Detach requested during dispatch waits until every
  * creation listener has observed the stable entry.
- * @param agent - the newly registered agent with its live session and completed setup.
+ * @param payload.agent - the newly registered agent with its live session and completed setup.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/created'(this: Scoped<Agent>, agent: Agent): void
+'agent/created'(this: Scoped<Agent>, payload: { agent: Agent }): void
 ```
 
 Types: [Scoped](scope.md)
@@ -702,11 +572,11 @@ An agent left the registry; AgentLoop emits this after driver quiescence and sco
  * An agent left the registry; AgentLoop emits this after driver quiescence
  * and scoped-registration unwind, but before session detachment. Custom
  * registry users own their driver-ordering contract.
- * @param agent - the exact agent removed from the registry.
+ * @param payload.agent - the exact agent removed from the registry.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/disposed'(this: Scoped<Agent>, agent: Agent): void
+'agent/disposed'(this: Scoped<Agent>, payload: { agent: Agent }): void
 ```
 
 Types: [Scoped](scope.md)
@@ -717,138 +587,111 @@ Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/t
 
 #### `agent/error` — emit
 
-A step or turn errored. The machine reports a failure here (plus the logger) even when the error has no in-turn position for a durable record.
+A step or turn errored. The machine reports a failure here even when the error has no in-turn position for a durable record.
 
 ```ts cordis-catalog
 /**
- * A step or turn errored. The machine reports a failure here (plus the
- * logger) even when the error has no in-turn position for a durable record.
- * @param agent - the agent whose turn errored.
- * @param turn - the turn in which the failure surfaced.
- * @param step - the step at which the failure surfaced.
- * @param error - the failure, verbatim.
+ * A step or turn errored. The machine reports a failure here even when
+ * the error has no in-turn position for a durable record.
+ * @param payload.agent - the agent whose turn errored.
+ * @param payload.turn - the turn in which the failure surfaced.
+ * @param payload.step - the step at which the failure surfaced.
+ * @param payload.error - the failure, verbatim.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: unknown): void
+'agent/error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; error: unknown }): void
 ```
 
 Types: [Scoped](scope.md)
 
 Source: [`packages/core/agent/src/types.ts:289`](../../packages/core/agent/src/types.ts)
 
-<a id="agentinboxdequeue--emit"></a>
+<a id="agentinboxclaimed--emit"></a>
 
-#### `agent/inbox/dequeue` — emit
+#### `agent/inbox/claimed` — 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.
+One message left the inbox inside its open turn. If the proposed step is rejected, the claimed message ends here: it is neither discarded nor re-emitted as a user/message, and the turn closes without a step.
 
 ```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 item - the exact claimed occurrence.
+ * One message left the inbox inside its open turn. If the proposed step
+ * is rejected, the claimed message ends here: it is neither discarded nor
+ * re-emitted as a user/message, and the turn closes without a step.
+ * @param payload.agent - the agent whose inbox changed.
+ * @param payload.message - the claimed message.
+ * @param payload.turn - the owning turn.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/inbox/dequeue'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
+'agent/inbox/claimed'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage; turn: number }): void
 ```
 
-Types: [Scoped](scope.md)
+Types: [Scoped](scope.md) · [UserMessage](session.md)
 
 Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts)
 
-<a id="agentinboxdiscard--emit"></a>
+<a id="agentinboxdiscarded--emit"></a>
 
-#### `agent/inbox/discard` — emit
+#### `agent/inbox/discarded` — emit
 
-Pending inbox items were dropped without delivering them, so every enqueue occurrence receives exactly one terminal `agent/inbox/dequeue` OR `agent/inbox/discard`. `cancel()` without `keepInbox`, including disposal, emits this after `agent/cancel-requested` when applicable and before aborting the active work. Fires once per drop with every dropped item.
+One message was discarded from the live inbox.
 
 ```ts cordis-catalog
 /**
- * Pending inbox items were dropped without delivering them, so every
- * enqueue occurrence receives exactly one terminal `agent/inbox/dequeue` OR
- * `agent/inbox/discard`. `cancel()` without `keepInbox`, including disposal,
- * emits this after `agent/cancel-requested` when applicable and before
- * aborting the active work. Fires once per drop with every dropped item.
- * @param agent - the agent whose inbox items were dropped.
- * @param items - the discarded occurrences in FIFO order (queued then steering); never empty.
+ * One message was discarded from the live inbox.
+ * @param payload.agent - the agent whose inbox changed.
+ * @param payload.message - the discarded message.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/inbox/discard'(this: Scoped<Agent>, agent: Agent, items: InboxItem[]): void
+'agent/inbox/discarded'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
 ```
 
-Types: [Scoped](scope.md)
+Types: [Scoped](scope.md) · [UserMessage](session.md)
 
 Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts)
 
-<a id="agentinboxenqueue--emit"></a>
+<a id="agentinboxinserted--emit"></a>
 
-#### `agent/inbox/enqueue` — emit
+#### `agent/inbox/inserted` — emit
 
-An item entered the queued or steering inbox. `placement` is the acceptance-time routing result; listeners must not reconstruct it from later agent or session state.
+One message entered the live inbox.
 
 ```ts cordis-catalog
 /**
- * An item entered the queued or steering inbox. `placement` is the
- * acceptance-time routing result; listeners must not reconstruct it from
- * later agent or session state.
- * @param agent - the owning agent.
- * @param item - accepted occurrence, message, and resolved placement.
+ * One message entered the live inbox.
+ * @param payload.agent - the agent whose inbox changed.
+ * @param payload.message - the inserted message.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
+'agent/inbox/inserted'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
 ```
 
-Types: [Scoped](scope.md)
+Types: [Scoped](scope.md) · [UserMessage](session.md)
 
 Source: [`packages/core/agent/src/types.ts:185`](../../packages/core/agent/src/types.ts)
 
-<a id="agentinboxupdate--emit"></a>
-
-#### `agent/inbox/update` — emit
-
-A still-pending queued item changed content. The item id, placement, and position remain stable while the event carries the replacement message.
-
-```ts cordis-catalog
-/**
- * A still-pending queued item changed content. The item id, placement, and
- * position remain stable while the event carries the replacement message.
- * @param agent - the owning agent.
- * @param item - the complete post-update occurrence.
- * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
- */
-'agent/inbox/update'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
-```
-
-Types: [Scoped](scope.md)
-
-Source: [`packages/core/agent/src/types.ts:321`](../../packages/core/agent/src/types.ts)
-
-<a id="agentprompt-submit--waterfall"></a>
+<a id="agentpre-step--waterfall"></a>
 
-#### `agent/prompt-submit` — waterfall
+#### `agent/pre-step` — waterfall
 
-Allow, rewrite, or block one claimed prompt before it becomes a user message or opens a turn. Call `next()` for the unchanged default. The signal controls only this admission attempt; listeners may cooperate with it but must not retain it for a later attempt or turn.
+Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages.
 
 ```ts cordis-catalog
 /**
- * Allow, rewrite, or block one claimed prompt before it becomes a user
- * message or opens a turn. Call `next()` for the unchanged default. The
- * signal controls only this admission attempt; listeners may cooperate with
- * it but must not retain it for a later attempt or turn.
- * @param agent - the agent whose turn claimed the message.
- * @param message - the frozen claimed message, including identity and source.
- * @param signal - the current turn's explicit abort signal.
+ * Reject a proposed step or replace the messages that enter it. Calling
+ * `next()` preserves the current messages.
+ * @param payload.agent - the agent proposing the step.
+ * @param payload.messages - messages removed from the inbox for this step.
+ * @param payload.turn - the turn that will own the step.
+ * @param payload.step - the step proposed by the loop.
+ * @param payload.signal - the current turn's cancellation signal.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode waterfall
  */
-'agent/prompt-submit'(this: Scoped<Agent>, agent: Agent, message: UserMessage, signal: AbortSignal, next: () => Promise<PromptDecision>): Promise<PromptDecision>
+'agent/pre-step'(this: Scoped<Agent>, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
 ```
 
 Types: [Scoped](scope.md) · [UserMessage](session.md)
@@ -867,14 +710,14 @@ Replace the frozen call configuration. `await next()` yields the config the mach
  * the machine would use (agent options on the first request, the logged
  * header afterwards); return a replacement to switch. Model-visible
  * content must use logged channels; this seam cannot mutate messages.
- * @param agent - the agent making the model call.
- * @param turn - the open turn number.
- * @param step - the step whose request this is.
- * @param signal - the current turn's explicit abort signal.
+ * @param payload.agent - the agent making the model call.
+ * @param payload.turn - the open turn number.
+ * @param payload.step - the step whose request this is.
+ * @param payload.signal - the current turn's explicit abort signal.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode waterfall
 */
-'agent/request'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
+'agent/request'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
 ```
 
 Types: [LlmCallConfig](llm-streaming.md) · [Scoped](scope.md)
@@ -885,28 +728,25 @@ Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/t
 
 #### `agent/request-error` — waterfall
 
-Handle a model-request failure after its failed step has closed but before the failed turn closes. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns the error, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.
+Handle one failed model-request attempt before the loop retries or closes its step. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns recovery, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.
 
 ```ts cordis-catalog
 /**
- * Handle a model-request failure after its failed step has closed but
- * before the failed turn closes. A listener returns `{ kind: 'retry' }`
- * without calling `next()` when it owns the error, or calls `next()` to
- * delegate. The default `undefined` leaves the failure terminal.
- * @param agent - the agent whose request failed.
- * @param turn - the open turn number.
- * @param step - the failed step number.
- * @param error - the original model-request failure.
- * @param failure - serializable facts normalized at the final adapter boundary.
- * @param priorFailures - immutable failures that already authorized another
- * retry turn in this consecutive sequence.
- * @param retryPolicy - immutable policy of the adapter registration that served
- * the failed request, or `undefined` if no final adapter served it.
- * @param signal - the turn abort signal.
+ * Handle one failed model-request attempt before the loop retries or closes
+ * its step. A listener returns `{ kind: 'retry' }` without calling `next()`
+ * when it owns recovery, or calls `next()` to delegate. The default
+ * `undefined` leaves the failure terminal.
+ * @param payload.agent - the agent whose request failed.
+ * @param payload.turn - the turn containing the failed request.
+ * @param payload.step - the step containing the failed request attempt.
+ * @param payload.provider - the provider selected for the failed request.
+ * @param payload.failure - serializable facts normalized at the final adapter boundary.
+ * @param payload.retryPolicy - the policy of the adapter registration that served the failed request.
+ * @param payload.signal - the turn abort signal.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode waterfall
  */
-'agent/request-error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, priorFailures: readonly LlmFailure[], retryPolicy: ResolvedRetryPolicy | undefined, signal: AbortSignal, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
+'agent/request-error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; provider: string; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined; signal: AbortSignal }, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
 ```
 
 Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md) · [Scoped](scope.md)
@@ -925,12 +765,12 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
  * `agent.inject()` to seed model-facing context. This is a notification, not
  * a veto; disposal requested by a lifecycle owner is rechecked before the
  * driver starts.
- * @param agent - the agent whose session lifecycle began.
- * @param source - why the session started (fresh startup, resume, …).
+ * @param payload.agent - the agent whose session lifecycle began.
+ * @param payload.source - why the session started (fresh startup, resume, …).
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/session-start'(this: Scoped<Agent>, agent: Agent, source: SessionStartSource): void
+'agent/session-start'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource }): void
 ```
 
 Types: [Scoped](scope.md)
@@ -941,18 +781,19 @@ Source: [`packages/core/agent/src/types.ts:216`](../../packages/core/agent/src/t
 
 #### `agent/status` — emit
 
-Agent status changed (`idle` ⇄ `running`). `send()` does not enter `running` synchronously; drive lifecycle from this event.
+Agent status changed (`idle` ⇄ `running`). A waking delivery enters `running` synchronously after reserving cancellation; `idle` means no driver remains scheduled or active.
 
 ```ts cordis-catalog
 /**
- * Agent status changed (`idle` ⇄ `running`). `send()` 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).
+ * Agent status changed (`idle` ⇄ `running`). A waking delivery enters
+ * `running` synchronously after reserving cancellation; `idle` means no
+ * driver remains scheduled or active.
+ * @param payload.agent - the agent whose status flipped.
+ * @param payload.status - the status just entered (the transition's destination).
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode emit
  */
-'agent/status'(this: Scoped<Agent>, agent: Agent, status: AgentStatus): void
+'agent/status'(this: Scoped<Agent>, payload: { agent: Agent; status: AgentStatus }): void
 ```
 
 Types: [Scoped](scope.md)
@@ -963,7 +804,7 @@ Source: [`packages/core/agent/src/types.ts:177`](../../packages/core/agent/src/t
 
 #### `agent/turn-stopping` — serial
 
-The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (`agent.steer(...)`) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying `concludesTurn` ends the turn at its step.
+The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (`agent.steer(...)`) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying `concludesTurn` ends the turn at its step. The conclusion never short-circuits already-submitted next-step work: same-step `additionalContexts` or racing steering still runs, and the turn closes only when that inbox drains.
 
 ```ts cordis-catalog
 /**
@@ -973,14 +814,17 @@ The turn is about to close: the model owes no response (no live tool calls, no f
  * re-reads its inbox: fresh steering runs another step, none closes the
  * turn. Data decides, so listener order cannot change the outcome. The
  * inverse control (stop a tool loop early) is data too: a tool result
- * carrying `concludesTurn` ends the turn at its step.
- * @param agent - the agent whose turn is at its stop boundary.
- * @param turn - the turn about to close.
- * @param signal - the current turn's explicit abort signal.
+ * carrying `concludesTurn` ends the turn at its step. The conclusion
+ * never short-circuits already-submitted next-step work: same-step
+ * `additionalContexts` or racing steering still runs, and the turn
+ * closes only when that inbox drains.
+ * @param payload.agent - the agent whose turn is at its stop boundary.
+ * @param payload.turn - the turn about to close.
+ * @param payload.signal - the current turn's explicit abort signal.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
  * @mode serial
  */
-'agent/turn-stopping'(this: Scoped<Agent>, agent: Agent, turn: number, signal: AbortSignal): Promise<void> | void
+'agent/turn-stopping'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; signal: AbortSignal }): Promise<void> | void
 ```
 
 Types: [Scoped](scope.md)
@@ -1003,12 +847,12 @@ A declarative agent entry failed before it could publish a live agent. Consumers
  * Consumers that buffer work for the configured identity use this
  * transient signal to reject that work instead of waiting forever. Normal
  * factory teardown suppresses failures from the cancelled startup attempt.
- * @param sessionId - exact shared agent/session identity that failed startup.
- * @param error - persistence, setup, or publication failure.
+ * @param payload.sessionId - exact shared agent/session identity that failed startup.
+ * @param payload.error - persistence, setup, or publication failure.
  * @mode emit
  */
-'agent-loop/config-start-failed'(sessionId: SessionId, error: unknown): void
+'agent-loop/config-start-failed'(payload: { sessionId: SessionId; error: unknown }): void
 ```
 
-Source: [`packages/core/agent-loop/src/index.ts:157`](../../packages/core/agent-loop/src/index.ts)
+Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 1 - 1
docs/subsystems/http-server.i18n.yaml

@@ -3,4 +3,4 @@
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/http-server.md
 http-server.md: b9795fe98b432b6ef5f7d01a4d3e115c809fe642
-http-server.zh.md: c3ba02dce5c58f491921f1fb31f999f9da808bc5
+http-server.zh.md: b84ea457bcd37eed361fde88fbf680d189aeebfb

+ 1 - 1
docs/subsystems/http-server.zh.md

@@ -2,7 +2,7 @@
 
 [English](http-server.md) | 中文
 
-[dsh-host-webserver](../../packages/host/webserver) 是 GUI 宿主 web 形态的 HTTP 载体:单个提供 `ctx.httpServer` 的 `node:http` 插件,由具名路由注册表加 index.html 转换挂点组成,兜底是静态 dist 回退。它不属于 agent loop(智能体循环)主干,也不是能力 seam:它不了解任何 harness 概念,每个功能表面(`/api` 桥接、插件 bundle、HMR(热模块替换)事件流)都是由其他插件注册的一条路由([分层说明](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md))。仅限 web(浏览器)形态:Electron 通过 `file://` 加载 dist,并经 IPC 桥接承载 fetch,不经过本服务器。
+[dsh-host-webserver](../../packages/host/webserver) 是 GUI 宿主 web 形态的 HTTP 载体:单个提供 `ctx.httpServer` 的 `node:http` 插件,由具名路由注册表、index.html 转换挂点与单一可认领的回退席位组成。它不属于 agent loop(智能体循环)主干,也不是能力 seam:它不了解任何 harness 概念,每个功能表面(`/api` 桥接、插件 bundle、HMR(热模块替换)事件流)都是由其他插件注册的一条路由([分层说明](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md))。仅限 web(浏览器)形态:Electron 通过 `file://` 加载 dist,并经 IPC 桥接承载 fetch,不经过本服务器。
 
 源码:[`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
 

+ 2 - 2
docs/subsystems/llm-streaming.i18n.yaml

@@ -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 docs/subsystems/llm-streaming.md
-llm-streaming.md: b3f850f4d07ad9e36d0e0db71dd9e0498e468767
-llm-streaming.zh.md: bb31c14b9bd0453399d83968b1062f67819caca3
+llm-streaming.md: c65416f449d1783f398cdd81b0d6987de83a6141
+llm-streaming.zh.md: ce79ecca53edf7213853df118489817451baa7b0

+ 174 - 19
docs/subsystems/llm-streaming.md

@@ -72,12 +72,84 @@ Where a message came from is itself a merge-extensible sum type:
  */
 interface MessageSourceMap {
   user: { kind: 'user' }
-  plugin: { kind: 'plugin'; plugin: string }
+  plugin: { kind: 'plugin'; plugin: string } & ContextFormed
   model: ModelMessageSource
   tool: ToolMessageSource
 }
 ```
 
+Provenance and shape are two independent axes. `kind` answers *who produced this*; the optional `form` a producer mixes in answers *what shape of information it is*, so several producers may share one presentation and one producer may emit more than one shape over a session. The vocabulary is semantic and grows one value at a time; an absent or unrecognized value is the documented default, presented as opaque content:
+
+```ts type-equiv
+/**
+ * What SHAPE of information a producer-supplied context carries, declared by
+ * the producer beside its provenance.
+ *
+ * `MessageSource.kind` answers *who produced this*; `form` answers *what kind
+ * of thing it is*, and the two axes are deliberately independent — several
+ * producers share one form (three snapshot producers today), and one producer
+ * may emit more than one form over a session.
+ *
+ * The vocabulary is SEMANTIC, never visual: a value states that the content is
+ * a file's instructions or a catalog of available items, and a consumer decides
+ * what that looks like. Colors, icons, ordering, and collapse defaults are the
+ * consumer's business and must not enter this union. It grows one value at a
+ * time as producers gain the structured fields their form needs; an absent or
+ * unknown value is the documented default, presented as opaque content.
+ */
+type ContextForm =
+  /** Instructions read out of workspace files the model is expected to follow. */
+  | 'instructions'
+  /** A catalog of items available in this session, republished as it changes. */
+  | 'catalog'
+  /** Current state, where a later snapshot from the same producer supersedes an earlier one. */
+  | 'snapshot'
+  /** A one-off account of something that just happened; it supersedes nothing. */
+  | 'notice'
+  /** A message another agent addressed to this one. */
+  | 'relay'
+  /** Material lifted out of another session's log, possibly reduced on the way in. */
+  | 'recall'
+```
+
+```ts type-equiv
+/** One named contribution to a `snapshot`-form context, in assembly order. */
+interface ContextSnapshotSection {
+  /** The contributing subsystem's name. */
+  readonly name: string
+  /** That contribution's model-facing text, exactly as assembled. */
+  readonly text: string
+}
+```
+
+```ts type-equiv
+/**
+ * Producer-declared {@link ContextForm} and the fields that form requires,
+ * mixed into the source shapes that carry one.
+ *
+ * Discriminated by `form` so a producer cannot declare a shape without the
+ * facts that shape is presented from: a `notice` must record its one-line
+ * account, a `snapshot` its sections. Omitting `form` stays valid — an
+ * undeclared context is the documented default.
+ */
+type ContextFormed =
+  | { readonly form?: never }
+  | { readonly form: 'instructions' }
+  | { readonly form: 'catalog' }
+  | {
+    readonly form: 'snapshot'
+    /** The named contributions this snapshot assembled, in order. */
+    readonly sections: readonly ContextSnapshotSection[]
+  }
+  | {
+    readonly form: 'notice'
+    /** One-line account of what happened, shown without expanding the row. */
+    readonly summary: string
+  }
+  | { readonly form: 'relay' }
+  | { readonly form: 'recall' }
+```
+
 ## `StreamChunk` — the raw protocol
 
 A streaming response interleaves several typed blocks (text, reasoning, multiple tool calls). `index` ties each delta to its block; `block-end` carries the fully-assembled `ContentBlock` so consumers don't have to re-assemble deltas themselves. It is a **closed** discriminated union — a `switch` over `type` ends with `assertNever`, so adding a variant breaks compilation at every consumer that must handle it.
@@ -87,8 +159,9 @@ A streaming response interleaves several typed blocks (text, reasoning, multiple
  * 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.
+ * afterward; tool arguments remain raw JSON strings. An adapter implementation
+ * may throw, but `LlmService.stream()` normalizes that failure to a terminal
+ * `error` or `aborted` finish before exposing it to consumers.
  */
 type StreamChunk =
   | { type: 'block-start'; index: number; blockType: ContentBlockType }
@@ -213,8 +286,9 @@ declare class BlockAssembler {
   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).
+   * @returns one block per seen index, except that max-token truncation drops
+   *   tool calls that cannot be executed safely; 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. */
@@ -301,6 +375,15 @@ interface LlmConfigurableProvider {
    * object; empty when the whole section is the profile.
    */
   settingsPath: readonly string[]
+  /**
+   * Whether the owning adapter knows this route only because configuration
+   * declared it — a gateway or self-hosted server it ships nothing about.
+   * Absent means the adapter draws no such distinction; false means it does
+   * and this route is one of its own. Only the adapter can answer: a stored
+   * profile is how a user-added route AND a corrected shipped one both look
+   * from outside.
+   */
+  declared?: boolean
 }
 ```
 
@@ -451,6 +534,55 @@ interface ToolSchema {
 
 The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition` that produces it (schema + `execute`) is on [tools.md](tools.md).
 
+A provider a surface is still drafting has no route and no catalog, so interrogation is described separately: the request carries the draft the user is editing, and the reply is candidates a surface may adopt rather than a catalog it must serve.
+
+```ts type-equiv
+/**
+ * One interrogation of a provider endpoint that configuration has not stored
+ * yet. Configuration surfaces send the draft a user is still editing, so the
+ * request carries the endpoint and credential directly instead of naming a
+ * route: a provider being added has no route to name.
+ */
+interface LlmModelDiscoveryRequest {
+  /**
+   * Route the draft is editing, when it edits an existing one. A route whose
+   * adapter already knows its models answers from that knowledge instead of
+   * asking the endpoint — the adapter's own registry is the better answer, and
+   * it costs no network call.
+   */
+  provider?: string
+  /**
+   * Endpoint to interrogate. Optional because a route the adapter already
+   * describes needs none; a route it does not must supply one.
+   */
+  baseURL?: string
+  /** Wire protocol the endpoint speaks, when the draft names one. */
+  api?: string
+  /** Credential for this interrogation alone; the harness never stores it. */
+  apiKey?: string
+  /** Caller cancellation; implementations must settle promptly after it aborts. */
+  signal?: AbortSignal
+}
+```
+
+```ts type-equiv
+/**
+ * One model an endpoint reports about itself. Every field but the id is
+ * optional because most provider listings disclose an id and nothing else;
+ * a surface adopting one of these still owes the capacities its adapter needs.
+ */
+interface LlmDiscoveredModel {
+  /** Model id the endpoint accepts. */
+  id: string
+  /** Human-readable name when the endpoint supplies one. */
+  name?: string
+  /** Maximum combined request and response context, when disclosed. */
+  contextWindow?: number
+  /** Maximum output tokens, when disclosed. */
+  maxTokens?: number
+}
+```
+
 ### The request envelope: `LlmCallConfig` and the logged header
 
 The loop builds each request from logged state. `EpochHeader` records call config, adapter-default provenance, rendered prompt, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
@@ -498,6 +630,8 @@ interface LlmCallConfigAdapterDefaults {
 interface PreparedLlmCall {
   /** Detached, deep-frozen config with any adapter-owned default materialized. */
   readonly config: LlmCallConfig
+  /** Immutable retry policy captured with the adapter registration. */
+  readonly retryPolicy: ResolvedRetryPolicy
   /** Detached context metadata resolved with the registration-bound call. */
   readonly context?: LlmModelContext
   /** Config fields materialized by the captured adapter rather than proposed by the caller. */
@@ -603,9 +737,9 @@ listProviders(): LlmProviderInfo[]
  * entry, or a provider already declared by any registration throws
  * `LlmError` without registering the rest. Disposed with the fiber.
  * @param entries - every configurable provider this plugin owns.
- * @returns the disposer that withdraws all of them.
+ * @returns a handle that withdraws all of them, and can atomically replace them.
  */
-registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): () => void
+registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle
 
 /**
  * List every declared configurable provider, registered or dormant.
@@ -613,6 +747,29 @@ registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): () =
  */
 listConfigurableProviders(): LlmConfigurableProvider[]
 
+/**
+ * Offer to interrogate provider endpoints on behalf of the settings
+ * namespace this plugin owns. The namespace is the key because that is what
+ * a configuration surface already holds from the configurable-provider
+ * directory, and because a provider being *added* has no route to name yet.
+ * Disposed with the fiber.
+ * @param settingsNs - the namespace whose profiles this discovery serves.
+ * @param discover - interrogates one endpoint; must honor `request.signal`.
+ * @returns the disposer that withdraws the offer.
+ */
+registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise<readonly LlmDiscoveredModel[]>, ): () => void
+
+/**
+ * Interrogate one provider endpoint for the models it advertises. The
+ * request describes a draft, not a stored route, so nothing here reads or
+ * writes settings or credentials — the caller owns both, and the reply is
+ * candidate metadata a surface may offer for adoption.
+ * @param settingsNs - namespace whose registered discovery serves this draft.
+ * @param request - the endpoint, protocol, and one-shot credential to use.
+ * @returns the advertised models, deduplicated in endpoint order.
+ */
+async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise<LlmDiscoveredModel[]>
+
 /**
  * Resolve the retry policy captured when one provider route was registered.
  * @param provider - registered provider route to inspect.
@@ -662,22 +819,20 @@ async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<Ll
 async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>
 
 /**
- * Stream one model call as raw chunks (token-level deltas). Throws
- * `LlmError` with code `NO_ADAPTER` if no adapter is registered for
- * `options.provider`. Replay state is retained only when the same adapter
- * instance owns its historical provider and the target provider. Final
- * adapter selection remains fixed through asynchronous exact-model resolution
- * and dispatch. Selection, dispatch, and iteration failures retain their
- * original Error identity and are tagged in a call-local scope for narrow
- * agent-loop request recovery; middleware and nested-call failures remain
- * untagged for the outer call.
+ * Stream one model call as raw chunks (token-level deltas). Replay state is
+ * retained only when the same adapter instance owns its historical provider
+ * and the target provider. Final adapter selection remains fixed through
+ * asynchronous exact-model resolution and dispatch. Adapter selection,
+ * dispatch, and iteration failures become terminal `error` or `aborted`
+ * finish chunks; middleware, nested-call, cleanup, and consumer failures
+ * remain thrown.
  * @param options - the full request; `options.provider` selects the adapter.
  * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
  */
 stream(options: GenerateOptions): AsyncIterable<StreamChunk>
 ```
 
-Source: [`packages/llm/llm/src/index.ts:232`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts)
 
 <a id="llm-events"></a>
 
@@ -702,7 +857,7 @@ The provider topology changed: an adapter registered or unregistered routes, or
 'llm/adapters-updated'(): void
 ```
 
-Source: [`packages/llm/llm/src/index.ts:71`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts)
 
 <a id="llmstream--waterfall"></a>
 
@@ -726,5 +881,5 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
 'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
 ```
 
-Source: [`packages/llm/llm/src/index.ts:60`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 174 - 19
docs/subsystems/llm-streaming.zh.md

@@ -72,12 +72,84 @@ interface Message {
  */
 interface MessageSourceMap {
   user: { kind: 'user' }
-  plugin: { kind: 'plugin'; plugin: string }
+  plugin: { kind: 'plugin'; plugin: string } & ContextFormed
   model: ModelMessageSource
   tool: ToolMessageSource
 }
 ```
 
+溯源与形态是相互独立的两根轴。`kind` 回答「由谁产生」;生产方可选混入的 `form` 回答「这是何种形态的信息」,因此多个生产方可以共用一种呈现,一个生产方在一次会话中也可以发出多种形态。该词汇表是语义的,逐个取值增长;未声明或无法识别的取值是有文档的默认,按不透明内容呈现:
+
+```ts type-equiv
+/**
+ * What SHAPE of information a producer-supplied context carries, declared by
+ * the producer beside its provenance.
+ *
+ * `MessageSource.kind` answers *who produced this*; `form` answers *what kind
+ * of thing it is*, and the two axes are deliberately independent — several
+ * producers share one form (three snapshot producers today), and one producer
+ * may emit more than one form over a session.
+ *
+ * The vocabulary is SEMANTIC, never visual: a value states that the content is
+ * a file's instructions or a catalog of available items, and a consumer decides
+ * what that looks like. Colors, icons, ordering, and collapse defaults are the
+ * consumer's business and must not enter this union. It grows one value at a
+ * time as producers gain the structured fields their form needs; an absent or
+ * unknown value is the documented default, presented as opaque content.
+ */
+type ContextForm =
+  /** Instructions read out of workspace files the model is expected to follow. */
+  | 'instructions'
+  /** A catalog of items available in this session, republished as it changes. */
+  | 'catalog'
+  /** Current state, where a later snapshot from the same producer supersedes an earlier one. */
+  | 'snapshot'
+  /** A one-off account of something that just happened; it supersedes nothing. */
+  | 'notice'
+  /** A message another agent addressed to this one. */
+  | 'relay'
+  /** Material lifted out of another session's log, possibly reduced on the way in. */
+  | 'recall'
+```
+
+```ts type-equiv
+/** One named contribution to a `snapshot`-form context, in assembly order. */
+interface ContextSnapshotSection {
+  /** The contributing subsystem's name. */
+  readonly name: string
+  /** That contribution's model-facing text, exactly as assembled. */
+  readonly text: string
+}
+```
+
+```ts type-equiv
+/**
+ * Producer-declared {@link ContextForm} and the fields that form requires,
+ * mixed into the source shapes that carry one.
+ *
+ * Discriminated by `form` so a producer cannot declare a shape without the
+ * facts that shape is presented from: a `notice` must record its one-line
+ * account, a `snapshot` its sections. Omitting `form` stays valid — an
+ * undeclared context is the documented default.
+ */
+type ContextFormed =
+  | { readonly form?: never }
+  | { readonly form: 'instructions' }
+  | { readonly form: 'catalog' }
+  | {
+    readonly form: 'snapshot'
+    /** The named contributions this snapshot assembled, in order. */
+    readonly sections: readonly ContextSnapshotSection[]
+  }
+  | {
+    readonly form: 'notice'
+    /** One-line account of what happened, shown without expanding the row. */
+    readonly summary: string
+  }
+  | { readonly form: 'relay' }
+  | { readonly form: 'recall' }
+```
+
 <a id="streamchunk--the-raw-protocol"></a>
 
 ## `StreamChunk`:原始协议
@@ -89,8 +161,9 @@ interface MessageSourceMap {
  * 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.
+ * afterward; tool arguments remain raw JSON strings. An adapter implementation
+ * may throw, but `LlmService.stream()` normalizes that failure to a terminal
+ * `error` or `aborted` finish before exposing it to consumers.
  */
 type StreamChunk =
   | { type: 'block-start'; index: number; blockType: ContentBlockType }
@@ -221,8 +294,9 @@ declare class BlockAssembler {
   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).
+   * @returns one block per seen index, except that max-token truncation drops
+   *   tool calls that cannot be executed safely; 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. */
@@ -309,6 +383,15 @@ interface LlmConfigurableProvider {
    * object; empty when the whole section is the profile.
    */
   settingsPath: readonly string[]
+  /**
+   * Whether the owning adapter knows this route only because configuration
+   * declared it — a gateway or self-hosted server it ships nothing about.
+   * Absent means the adapter draws no such distinction; false means it does
+   * and this route is one of its own. Only the adapter can answer: a stored
+   * profile is how a user-added route AND a corrected shipped one both look
+   * from outside.
+   */
+  declared?: boolean
 }
 ```
 
@@ -459,6 +542,55 @@ interface ToolSchema {
 
 面向模型的 `ToolSchema` 是协议格式;产出它的已注册 `ToolDefinition`(schema + `execute`)在 [tools.md](tools.md) 中。
 
+界面正在起草的提供方既没有路由也没有 catalog,因此询问被单独描述:请求携带用户正在编辑的草稿,回复是界面可以采纳的候选,而不是它必须服务的 catalog。
+
+```ts type-equiv
+/**
+ * One interrogation of a provider endpoint that configuration has not stored
+ * yet. Configuration surfaces send the draft a user is still editing, so the
+ * request carries the endpoint and credential directly instead of naming a
+ * route: a provider being added has no route to name.
+ */
+interface LlmModelDiscoveryRequest {
+  /**
+   * Route the draft is editing, when it edits an existing one. A route whose
+   * adapter already knows its models answers from that knowledge instead of
+   * asking the endpoint — the adapter's own registry is the better answer, and
+   * it costs no network call.
+   */
+  provider?: string
+  /**
+   * Endpoint to interrogate. Optional because a route the adapter already
+   * describes needs none; a route it does not must supply one.
+   */
+  baseURL?: string
+  /** Wire protocol the endpoint speaks, when the draft names one. */
+  api?: string
+  /** Credential for this interrogation alone; the harness never stores it. */
+  apiKey?: string
+  /** Caller cancellation; implementations must settle promptly after it aborts. */
+  signal?: AbortSignal
+}
+```
+
+```ts type-equiv
+/**
+ * One model an endpoint reports about itself. Every field but the id is
+ * optional because most provider listings disclose an id and nothing else;
+ * a surface adopting one of these still owes the capacities its adapter needs.
+ */
+interface LlmDiscoveredModel {
+  /** Model id the endpoint accepts. */
+  id: string
+  /** Human-readable name when the endpoint supplies one. */
+  name?: string
+  /** Maximum combined request and response context, when disclosed. */
+  contextWindow?: number
+  /** Maximum output tokens, when disclosed. */
+  maxTokens?: number
+}
+```
+
 ### 请求信封:`LlmCallConfig` 与记录的 header
 
 循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、适配器默认值来源、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
@@ -506,6 +638,8 @@ interface LlmCallConfigAdapterDefaults {
 interface PreparedLlmCall {
   /** Detached, deep-frozen config with any adapter-owned default materialized. */
   readonly config: LlmCallConfig
+  /** Immutable retry policy captured with the adapter registration. */
+  readonly retryPolicy: ResolvedRetryPolicy
   /** Detached context metadata resolved with the registration-bound call. */
   readonly context?: LlmModelContext
   /** Config fields materialized by the captured adapter rather than proposed by the caller. */
@@ -611,9 +745,9 @@ listProviders(): LlmProviderInfo[]
  * entry, or a provider already declared by any registration throws
  * `LlmError` without registering the rest. Disposed with the fiber.
  * @param entries - every configurable provider this plugin owns.
- * @returns the disposer that withdraws all of them.
+ * @returns a handle that withdraws all of them, and can atomically replace them.
  */
-registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): () => void
+registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle
 
 /**
  * List every declared configurable provider, registered or dormant.
@@ -621,6 +755,29 @@ registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): () =
  */
 listConfigurableProviders(): LlmConfigurableProvider[]
 
+/**
+ * Offer to interrogate provider endpoints on behalf of the settings
+ * namespace this plugin owns. The namespace is the key because that is what
+ * a configuration surface already holds from the configurable-provider
+ * directory, and because a provider being *added* has no route to name yet.
+ * Disposed with the fiber.
+ * @param settingsNs - the namespace whose profiles this discovery serves.
+ * @param discover - interrogates one endpoint; must honor `request.signal`.
+ * @returns the disposer that withdraws the offer.
+ */
+registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise<readonly LlmDiscoveredModel[]>, ): () => void
+
+/**
+ * Interrogate one provider endpoint for the models it advertises. The
+ * request describes a draft, not a stored route, so nothing here reads or
+ * writes settings or credentials — the caller owns both, and the reply is
+ * candidate metadata a surface may offer for adoption.
+ * @param settingsNs - namespace whose registered discovery serves this draft.
+ * @param request - the endpoint, protocol, and one-shot credential to use.
+ * @returns the advertised models, deduplicated in endpoint order.
+ */
+async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise<LlmDiscoveredModel[]>
+
 /**
  * Resolve the retry policy captured when one provider route was registered.
  * @param provider - registered provider route to inspect.
@@ -670,22 +827,20 @@ async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<Ll
 async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>
 
 /**
- * Stream one model call as raw chunks (token-level deltas). Throws
- * `LlmError` with code `NO_ADAPTER` if no adapter is registered for
- * `options.provider`. Replay state is retained only when the same adapter
- * instance owns its historical provider and the target provider. Final
- * adapter selection remains fixed through asynchronous exact-model resolution
- * and dispatch. Selection, dispatch, and iteration failures retain their
- * original Error identity and are tagged in a call-local scope for narrow
- * agent-loop request recovery; middleware and nested-call failures remain
- * untagged for the outer call.
+ * Stream one model call as raw chunks (token-level deltas). Replay state is
+ * retained only when the same adapter instance owns its historical provider
+ * and the target provider. Final adapter selection remains fixed through
+ * asynchronous exact-model resolution and dispatch. Adapter selection,
+ * dispatch, and iteration failures become terminal `error` or `aborted`
+ * finish chunks; middleware, nested-call, cleanup, and consumer failures
+ * remain thrown.
  * @param options - the full request; `options.provider` selects the adapter.
  * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
  */
 stream(options: GenerateOptions): AsyncIterable<StreamChunk>
 ```
 
-Source: [`packages/llm/llm/src/index.ts:232`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts)
 
 <a id="llm-events"></a>
 
@@ -710,7 +865,7 @@ The provider topology changed: an adapter registered or unregistered routes, or
 'llm/adapters-updated'(): void
 ```
 
-Source: [`packages/llm/llm/src/index.ts:71`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts)
 
 <a id="llmstream--waterfall"></a>
 
@@ -734,5 +889,5 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
 'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
 ```
 
-Source: [`packages/llm/llm/src/index.ts:60`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 2 - 2
docs/subsystems/permission.i18n.yaml

@@ -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 docs/subsystems/permission.md
-permission.md: 60ed88a79d0e51922f4cdffafa8b547669b49f5d
-permission.zh.md: c06714fc6a111f56af680c3dea1764c90f09998d
+permission.md: 6f9a37dbb784ccbaea74056519992e40f9c07c6d
+permission.zh.md: 8eda50fae09a67c41003d7a914fb139c4132a900

+ 1 - 1
docs/subsystems/permission.md

@@ -125,7 +125,7 @@ optionOf(name: string): PresetOption
 set(session: Session, name: string): void
 ```
 
-Types: [Session](session.md) · [SessionEvent](core.md)
+Types: [Session](session.md) · [SessionEvent](session.md)
 
 Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 1 - 1
docs/subsystems/permission.zh.md

@@ -125,7 +125,7 @@ optionOf(name: string): PresetOption
 set(session: Session, name: string): void
 ```
 
-Types: [Session](session.md) · [SessionEvent](core.md)
+Types: [Session](session.md) · [SessionEvent](session.md)
 
 Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 2 - 2
docs/subsystems/persistence.i18n.yaml

@@ -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 docs/subsystems/persistence.md
-persistence.md: 1edf3c88fbabe55058bbf94ed8f31decf47cb84a
-persistence.zh.md: dbebd9ff819c8d50cfa9748678dac09c86adde6d
+persistence.md: 580ec250ebad3f2b51982f98a611268f26487321
+persistence.zh.md: 6676bf26e6eb0fb8f7dd9a94d17d4d70d1d1b271

+ 1 - 1
docs/subsystems/persistence.md

@@ -332,7 +332,7 @@ abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
 abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
 ```
 
-Types: [SessionEvent](core.md) · [SessionId](core.md)
+Types: [SessionEvent](session.md) · [SessionId](core.md)
 
 Source: [`packages/session/session-persistence/src/index.ts:72`](../../packages/session/session-persistence/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 1 - 1
docs/subsystems/persistence.zh.md

@@ -332,7 +332,7 @@ abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
 abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
 ```
 
-Types: [SessionEvent](core.md) · [SessionId](core.md)
+Types: [SessionEvent](session.md) · [SessionId](core.md)
 
 Source: [`packages/session/session-persistence/src/index.ts:72`](../../packages/session/session-persistence/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 2 - 2
docs/subsystems/session-projection.i18n.yaml

@@ -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 docs/subsystems/session-projection.md
-session-projection.md: 56f9fb43dddc482264aab52ceeb9b9920df148cd
-session-projection.zh.md: 4a2310fed4af6f018af07b5f2b46d3c28aa361a4
+session-projection.md: 64351195028c62f623b32cbeb80c952795c1e9e6
+session-projection.zh.md: cf9036e6db3f82d84ffa3b8aa809c5617409659a

+ 1 - 1
docs/subsystems/session-projection.md

@@ -256,7 +256,7 @@ viewCheckpoint(checkpoint: ProjectionCheckpoint): Partial<SessionProjectionMap>
 restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
 ```
 
-Types: [Session](session.md) · [SessionEvent](core.md)
+Types: [Session](session.md) · [SessionEvent](session.md)
 
 Source: [`packages/session/session-projection/src/index.ts:156`](../../packages/session/session-projection/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 1 - 1
docs/subsystems/session-projection.zh.md

@@ -256,7 +256,7 @@ viewCheckpoint(checkpoint: ProjectionCheckpoint): Partial<SessionProjectionMap>
 restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
 ```
 
-Types: [Session](session.md) · [SessionEvent](core.md)
+Types: [Session](session.md) · [SessionEvent](session.md)
 
 Source: [`packages/session/session-projection/src/index.ts:156`](../../packages/session/session-projection/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 2 - 2
docs/subsystems/session.i18n.yaml

@@ -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 docs/subsystems/session.md
-session.md: 28b3f959b47a24373860c2334f9273286d072c95
-session.zh.md: 5870265f55892231700b65ac6ed69d827a0ab1ec
+session.md: 7735e90807f6fce1b3645096437b3e86189dc8ec
+session.zh.md: 09f34ac19c2f685bf5f8e5b11355b4c642c584d0

+ 1 - 1
docs/subsystems/session.md

@@ -804,7 +804,7 @@ Post-commit, fire-and-forget append feed. The listener snapshot resolves before
 'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void
 ```
 
-Types: [Scoped](scope.md) · [SessionEvent](core.md)
+Types: [Scoped](scope.md)
 
 Source: [`packages/core/session/src/index.ts:96`](../../packages/core/session/src/index.ts)
 

+ 1 - 1
docs/subsystems/session.zh.md

@@ -808,7 +808,7 @@ Post-commit, fire-and-forget append feed. The listener snapshot resolves before
 'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void
 ```
 
-Types: [Scoped](scope.md) · [SessionEvent](core.md)
+Types: [Scoped](scope.md)
 
 Source: [`packages/core/session/src/index.ts:96`](../../packages/core/session/src/index.ts)
 

+ 2 - 2
docs/subsystems/subagent.i18n.yaml

@@ -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 docs/subsystems/subagent.md
-subagent.md: c132c073459642b133e0d7e662ee98a8c9dfb694
-subagent.zh.md: e46f9aa98389842918ae37dda1477ef1b565e774
+subagent.md: 3bf3ac7c48bfa08be86314bb43263d4ee6198654
+subagent.zh.md: ddc3a715fbb6589dffc38f0e0c8b23bf35dddf6e

+ 2 - 2
docs/subsystems/subagent.md

@@ -133,7 +133,7 @@ persisted Session
 | `waiting` | wake the same Activation |
 | no Activation | cold-resume a new Activation |
 
-`running` means the Agent has an active admission or turn, or waking inbox work; `waiting` means it is quiescent but still owns at least one child Activation that has not completed disposal; `settled` means quiescent with every owned child disposed, at which point the manager disposes the `AgentHandle` and removes the Activation. The manager derives these internal conditions from Agent quiescence and the owned-child set rather than maintaining a second execution state machine.
+`running` means the Agent has an active admission or turn, or waking inbox work; `waiting` means it is quiescent but still owns at least one child Activation that has not completed disposal; `settled` means quiescent with every owned child disposed, at which point the manager disposes the [`AgentHandle`](core.md#creation-and-ownership) and removes the Activation. The manager derives these internal conditions from Agent quiescence and the owned-child set rather than maintaining a second execution state machine.
 
 The Agent inbox is the only queue. Every continuation message becomes one `Agent.followup()` FIFO turn, so accepted messages have one observable order and a follow-up cannot redirect a turn already underway. Successful delivery returns the accepted `MessageId`; the existing `agent/inbox/enqueue`, `agent/inbox/dequeue`, and `agent/inbox/discard` events remain the message-lifecycle observations, and the continuation layer defines no subagent-specific delivery route.
 
@@ -432,7 +432,7 @@ Provider `start()` fulfills with a published run. The service mints a unique `ru
 The spawn and fork backends create an ordinary one-shot agent through `parent.ctx`, pass cancellation into core creation, and dispose through `AgentHandle`; a continuable child is instead created by the continuation manager through its own activation-owner scope. Provider removal blocks new starts without revoking accepted runs. Each child gets a new flat scope rather than inheriting parent registrations. Depth and fork seeding reuse existing agent and session vocabulary:
 
 - **Delegation depth** is durable `SessionHeader.delegationDepth` plus the merge-extensible runtime field `AgentOptions.subagentDepth`; absence means top-level depth zero, and the greater present value is authoritative. The seam owns both fields — the loop neither sets nor reads them — so an in-process child persists parent depth + 1, cold resume cannot lower it, and every start rejects a derived depth outside the safe-integer domain or above a defined absolute `request.maxDepth` cap.
-- **Fork seeding** uses `CreateAgentOptions.seed` (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `ctx.agents.resume()` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and the [invariants](../../packages/support/invariants) replay accepts it (the in-flight, unbalanced turn is excluded).
+- **Fork seeding** uses [`CreateAgentOptions.seed`](core.md#creation-and-ownership) (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `ctx.agents.resume()` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and the [invariants](../../packages/support/invariants) replay accepts it (the in-flight, unbalanced turn is excluded).
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 2 - 2
docs/subsystems/subagent.zh.md

@@ -133,7 +133,7 @@ persisted Session
 | `waiting` | 唤醒同一 Activation |
 | 无 Activation | 冷恢复一个新的 Activation |
 
-`running` 表示 Agent 拥有活跃的准入或轮次,或正在唤醒收件箱工作;`waiting` 表示它已停稳,但仍拥有至少一个尚未完成 dispose 的子 Activation;`settled` 表示已停稳且其拥有的每个子级都已 dispose,此时管理器会 dispose `AgentHandle` 并移除该 Activation。管理器根据 Agent 的完全停稳状态与其拥有的子级集合推导这些内部条件,而非维护第二套执行状态机。
+`running` 表示 Agent 拥有活跃的准入或轮次,或正在唤醒收件箱工作;`waiting` 表示它已停稳,但仍拥有至少一个尚未完成 dispose 的子 Activation;`settled` 表示已停稳且其拥有的每个子级都已 dispose,此时管理器会 dispose [`AgentHandle`](core.md#creation-and-ownership) 并移除该 Activation。管理器根据 Agent 的完全停稳状态与其拥有的子级集合推导这些内部条件,而非维护第二套执行状态机。
 
 Agent 收件箱是唯一的队列。每条继续执行消息都会成为一个 `Agent.followup()` FIFO 轮次,因此已接受的消息共享同一个可观测顺序,且后续消息无法改变已在进行中的轮次。投递成功会返回被接受的 `MessageId`;既有的 `agent/inbox/enqueue`、`agent/inbox/dequeue` 与 `agent/inbox/discard` 事件仍是消息生命周期的观测点,继续执行层不定义任何 subagent 专属的投递路由。
 
@@ -434,7 +434,7 @@ interface SubagentProvider {
 spawn 和 fork 后端通过 `parent.ctx` 创建一个普通的单次 agent,将取消信号传入核心创建流程,并通过 `AgentHandle` 进行 dispose;而可继续子 agent 则由继续执行管理器通过其自己的 activation-owner 作用域创建。移除提供方会阻止新的 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 })` 传递,与 `ctx.agents.resume()` 使用的原语相同)。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*——父级事件直到并包括其最后一个 `turn/end`——因此种子从 0 连续,[invariants](../../packages/support/invariants) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。
+- **Fork 种子注入**使用 [`CreateAgentOptions.seed`](core.md#creation-and-ownership)(一个 `SessionEvent[]` 前缀,经由 `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })` 传递,与 `ctx.agents.resume()` 使用的原语相同)。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*——父级事件直到并包括其最后一个 `turn/end`——因此种子从 0 连续,[invariants](../../packages/support/invariants) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 2 - 2
docs/subsystems/tools.i18n.yaml

@@ -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 docs/subsystems/tools.md
-tools.md: b63014d1c2058c8fc7e96817d4bca858a1954482
-tools.zh.md: 2dc5802efce25a732a60c660191cd584ede623bb
+tools.md: 0551ee1c9dfa12b415c1f4575b5225f771cb53bc
+tools.zh.md: 50843b07f1d98b1c9d9222288b1273b34bee1e4a

+ 1 - 1
docs/subsystems/tools.md

@@ -2,7 +2,7 @@
 
 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.
+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; the model-facing [`ToolSchema`](llm-streaming.md#the-model-request-and-result) wire shape is declared with the model request. 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)
 

+ 1 - 1
docs/subsystems/tools.zh.md

@@ -2,7 +2,7 @@
 
 [English](tools.md) | 中文
 
-[dsh-tools](../../packages/core/tools) 的工具流水线。[core.md](core.md) 介绍了 `ToolDefinition`(唯一被提升到主干的流水线编写类型)和 `ToolSchema`(面向模型的协议格式(wire format)形状)。本页拥有完整的 `ToolDefinition`、用于构建它的类型化 schema DSL、受保护的执行形状,以及 UI 展示词汇。
+[dsh-tools](../../packages/core/tools) 的工具流水线。[core.md](core.md) 介绍了 `ToolDefinition`(唯一被提升到主干的流水线编写类型);面向模型的 [`ToolSchema`](llm-streaming.md#the-model-request-and-result) 协议格式(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)
 

+ 2 - 2
packages/interaction/README.i18n.yaml

@@ -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 packages/interaction/README.md
-README.md: 31a306cb08fe69297b0d0843b57b5e491c0bbe44
-README.zh.md: 6eafe8726ba59edd3a6bf739deacc037a9b56b76
+README.md: a72cd48bb1d3ec15b1116d576fac86619df59528
+README.zh.md: 170a557e40befaa0f7073f25e8a2b372958a6ec1

+ 1 - 1
packages/interaction/README.md

@@ -14,4 +14,4 @@ The seams through which a human collaborates with a running agent — questions,
 
 These packages integrate through existing agent and session contracts rather than changing the loop. Interactive applications provide the concrete command, approval, and question adapters; automation uses [`acp/`](../acp/README.md), and runnable demo bundles live under [`examples/`](../examples/README.md). The product [`dsh`](../../apps/cli/README.md) CLI composes these packages directly.
 
-The subsystem references: [approval.md](../../docs/subsystems/approval.md), [permission.md](../../docs/subsystems/permission.md), [user-interaction.md](../../docs/subsystems/user-interaction.md), [commands.md](../../docs/subsystems/commands.md), and [tui.md](../../docs/subsystems/tui.md). The automation-only ACP transport is [`acp/`](../acp/README.md), the SDK's JSON-RPC server half [`scaffold/server`](../scaffold/README.md), and the shared bin boot glue [`boot/`](../boot/README.md).
+The subsystem references: [approval.md](../../docs/subsystems/approval.md), [permission.md](../../docs/subsystems/permission.md), [user-interaction.md](../../docs/subsystems/user-interaction.md), and [commands.md](../../docs/subsystems/commands.md). The automation-only ACP transport is [`acp/`](../acp/README.md), the SDK's JSON-RPC server half [`scaffold/server`](../scaffold/README.md), and the shared bin boot glue [`boot/`](../boot/README.md).

+ 1 - 1
packages/interaction/README.zh.md

@@ -14,4 +14,4 @@
 
 这些包通过现有的 agent(智能体)和会话契约集成,而不改变循环。交互式应用提供具体的命令、审批和提问适配器;自动化使用 [`acp/`](../acp/README.md),可运行的演示组合包位于 [`examples/`](../examples/README.md)。产品 [`dsh`](../../apps/cli/README.md) CLI(命令行界面)直接组合这些包。
 
-子系统参考:[approval.md](../../docs/subsystems/approval.md)、[permission.md](../../docs/subsystems/permission.md)、[user-interaction.md](../../docs/subsystems/user-interaction.md)、[commands.md](../../docs/subsystems/commands.md) 与 [tui.md](../../docs/subsystems/tui.md)。仅自动化的 ACP 传输在 [`acp/`](../acp/README.md),SDK 的 JSON-RPC 服务器一半在 [`scaffold/server`](../scaffold/README.md),共享 bin 启动胶水在 [`boot/`](../boot/README.md)。
+子系统参考:[approval.md](../../docs/subsystems/approval.md)、[permission.md](../../docs/subsystems/permission.md)、[user-interaction.md](../../docs/subsystems/user-interaction.md)与 [commands.md](../../docs/subsystems/commands.md)。仅自动化的 ACP 传输在 [`acp/`](../acp/README.md),SDK 的 JSON-RPC 服务器一半在 [`scaffold/server`](../scaffold/README.md),共享 bin 启动胶水在 [`boot/`](../boot/README.md)。

+ 1 - 1
packages/self-modification/README.i18n.yaml

@@ -3,4 +3,4 @@
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/self-modification/README.md
 README.md: 53f700993cce8df8729fba5a92a9498fff2412cd
-README.zh.md: 90030cda33f47d128041b7619546ac1309b8465a
+README.zh.md: 18e03798b90e73f1d7e61ecf7010b8c472454a08

+ 0 - 2
packages/self-modification/README.zh.md

@@ -8,5 +8,3 @@ agent 修改自身运行时:检查已加载的插件与服务接口、挂载
 |---|---|---|
 | [`tool-cordis/`](tool-cordis/README.md) | `cordis_inspect`/`cordis_mount`/`cordis_unmount` 工具:读取当前进程运行时,并在一个自有分组 fiber 下管理内存中的临时插件 | 注册到 `ctx.tools` |
 | [`repository-plugin/`](repository-plugin/README.md) | 通过 DSH 自有子 Plugin 准备并挂载静态 repository skills 与通用 `.mcp.json` server | 注册一个 Loader builtin |
-
-工具契约见 [`tool-cordis/README.md`](tool-cordis/README.md) 与 [`repository-plugin/README.md`](repository-plugin/README.md)。

+ 5 - 8
scripts/gen-cordis-catalog.ts

@@ -154,16 +154,17 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
 export const LINK_MAP: Readonly<Record<string, string>> = {
   Agent: 'core.md',
   AgentCancelCause: 'core.md',
+  AgentFactory: 'core.md',
+  AgentHandle: 'core.md',
   AgentOptions: 'core.md',
   AgentStatus: 'core.md',
   ContentBlock: 'llm-streaming.md',
-  ContinuationDecision: 'core.md',
-  ContinuationStop: 'core.md',
+  CreateAgentOptions: 'core.md',
   GenerateOptions: 'llm-streaming.md',
   InboxItem: 'core.md',
   InboxPlacement: 'core.md',
   MessageId: 'llm-streaming.md',
-  HookContext: 'core.md',
+  ResumeAgentOptions: 'core.md',
   SettleReason: 'core.md',
   AdapterRegistrationHandle: 'llm-streaming.md',
   DirectoryRegistrationHandle: 'llm-streaming.md',
@@ -188,7 +189,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
   PreparedReferencedMessage: 'session-reference.md',
   SessionReferenceCandidate: 'session-reference.md',
   SessionReferenceInput: 'session-reference.md',
-  SessionEvent: 'core.md',
+  SessionEvent: 'session.md',
   SessionId: 'core.md',
   SessionStartSource: 'core.md',
   SessionLogSnapshot: 'session-query.md',
@@ -410,18 +411,15 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
 
 /** Project types deliberately documented outside the subsystems catalog. */
 export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
-  AgentFactory: 'agent creation seam is owned by packages/core/agent/README.md',
   z: 'schemastery schema constructor is owned by vendor/schemastery (vendored upstream)',
   BeginCommandRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
   InsertReferenceRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
   ConsumeTokenRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
   InsertTextRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
-  AgentHandle: 'agent ownership handle is owned by packages/core/agent/README.md',
   BashEnvContributor: 'service-local extension type is owned by packages/bash/tool-bash/src/index.ts',
   BashEnvVariableInfo: 'service-local metadata type is owned by packages/bash/tool-bash/src/index.ts',
   CompactAgentContext: 'compaction service input is owned by packages/compact/compact/src/index.ts',
   ManualCompactAgentContext: 'manual compaction service input is owned by packages/compact/compact/src/index.ts',
-  CreateAgentOptions: 'agent creation contract is owned by packages/core/agent/README.md',
   DomainImpl: 'domain implementation contract is owned by packages/storage/storage-domain/README.md',
   CommandExecution: 'executor return contract is owned by packages/interaction/commands/src/index.ts',
   'z.core.JSONSchema.BaseSchema': 'zod projection output is owned by the zod v4 API',
@@ -437,7 +435,6 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
   KnobState: 'projection unit state shape is owned by packages/interaction/permission/README.md',
   PermissionSelect: 'permissions projection payload is owned by packages/interaction/permission/src/types.ts',
   PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md',
-  ResumeAgentOptions: 'agent resume contract is owned by packages/core/agent/README.md',
   Sandbox: 'external E2B SDK handle is owned by packages/e2b/e2b/README.md',
   SessionForkSource: 'service-local fork input is owned by packages/core/session/src/index.ts',
   SubagentRunEndInfo: 'event payload contract is owned by packages/subagent/subagent/src/types.ts',

+ 15 - 2
scripts/project-doc-site.spec.ts

@@ -1,9 +1,9 @@
 /** Tests for the documentation website projection adapter. */
 
 import { execFileSync } from 'node:child_process'
-import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
+import { existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
 import { tmpdir } from 'node:os'
-import { join, resolve } from 'node:path'
+import { basename, join, resolve } from 'node:path'
 import { afterEach, describe, expect, it } from 'vitest'
 import { docsPages, type DocsPage } from '../website/docs.ts'
 import {
@@ -269,6 +269,19 @@ describe('docsPages locale routes', () => {
     }
   })
 
+  it('indexes every subsystem page in both sides of the folder README', () => {
+    const pages = globSync(join(repositoryRoot, 'docs/subsystems/*.md'))
+      .map(page => basename(page))
+      .filter(page => !page.endsWith('.zh.md') && page !== 'README.md')
+      .sort()
+    expect(pages.length).toBeGreaterThan(0)
+    for (const readme of ['README.md', 'README.zh.md']) {
+      const rows = readFileSync(join(repositoryRoot, 'docs/subsystems', readme), 'utf8')
+      const missing = pages.filter(page => !rows.includes(`| [${page}](${page}) |`))
+      expect(missing, `${readme} must carry one table row per subsystem page`).toEqual([])
+    }
+  })
+
   it('projects translated subsystem pages while retaining explicit English fallbacks', () => {
     const rootPages = docsPages.filter(page => (
       page.locale === 'root' && page.route.startsWith('reference/subsystems/')

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
scripts/snapshots/translation-prompt-v4/request-response.expected.json


+ 20 - 10
scripts/type-equiv.manifest.json

@@ -22,17 +22,17 @@
       "source": "packages/llm/llm/src/message.ts"
     },
     {
-      "doc": "docs/subsystems/core.md",
+      "doc": "docs/subsystems/llm-streaming.md",
       "symbol": "ContextForm",
       "source": "packages/llm/llm/src/message.ts"
     },
     {
-      "doc": "docs/subsystems/core.md",
+      "doc": "docs/subsystems/llm-streaming.md",
       "symbol": "ContextSnapshotSection",
       "source": "packages/llm/llm/src/message.ts"
     },
     {
-      "doc": "docs/subsystems/core.md",
+      "doc": "docs/subsystems/llm-streaming.md",
       "symbol": "ContextFormed",
       "source": "packages/llm/llm/src/message.ts"
     },
@@ -57,12 +57,12 @@
       "source": "packages/llm/llm/src/types.ts"
     },
     {
-      "doc": "docs/subsystems/core.md",
+      "doc": "docs/subsystems/llm-streaming.md",
       "symbol": "LlmModelDiscoveryRequest",
       "source": "packages/llm/llm/src/types.ts"
     },
     {
-      "doc": "docs/subsystems/core.md",
+      "doc": "docs/subsystems/llm-streaming.md",
       "symbol": "LlmDiscoveredModel",
       "source": "packages/llm/llm/src/types.ts"
     },
@@ -111,11 +111,6 @@
       "symbol": "LlmCallConfigAdapterDefaults",
       "source": "packages/llm/llm/src/call-config.ts"
     },
-    {
-      "doc": "docs/subsystems/core.md",
-      "symbol": "SessionEvent",
-      "source": "packages/core/session/src/types.ts"
-    },
     {
       "doc": "docs/subsystems/core.md",
       "symbol": "InboxTarget",
@@ -1689,6 +1684,21 @@
       "doc": "docs/subsystems/session-projection.md",
       "symbol": "ProjectionChangeListener",
       "source": "packages/session/session-projection/src/index.ts"
+    },
+    {
+      "doc": "docs/subsystems/core.md",
+      "symbol": "AgentHandle",
+      "source": "packages/core/agent/src/index.ts"
+    },
+    {
+      "doc": "docs/subsystems/core.md",
+      "symbol": "AgentStatus",
+      "source": "packages/core/agent/src/types.ts"
+    },
+    {
+      "doc": "docs/subsystems/core.md",
+      "symbol": "AgentOptions",
+      "source": "packages/core/agent/src/types.ts"
     }
   ]
 }

+ 1 - 1
website/docs.ts

@@ -261,6 +261,7 @@ const subsystemsReference = pairedPages(([
   ['README.md', '子系统', 'Subsystems', 0],
   ['core.md', '核心', 'Core', 1],
   ['scope.md', '作用域', 'Scopes', 2],
+  ['typert.md', 'TypeRT', 'TypeRT', 39],
   ['session.md', '会话', 'Sessions', 3],
   ['session-query.md', '会话查询', 'Session query', 4],
   ['session-reference.md', '会话引用', 'Session references', 5],
@@ -293,7 +294,6 @@ const subsystemsReference = pairedPages(([
   ['workspace.md', '工作区', 'Workspaces', 32],
   ['http-server.md', 'HTTP 服务器', 'HTTP server', 33],
   ['client-modules.md', '客户端模块', 'Client modules', 34],
-  ['tui.md', 'TUI 扩展', 'TUI extensions', 35],
   ['invariants.md', '运行时不变式', 'Runtime invariants', 36],
   ['session-projection.md', '会话投影', 'Session projections', 37],
   ['telemetry.md', '遥测', 'Telemetry', 38],

Деякі файли не було показано, через те що забагато файлів було змінено