diff --git a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml index efeb9d72f3..336d9669ed 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.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/architecture/2026-07-05-subagent-provider-lifecycle-events.md -2026-07-05-subagent-provider-lifecycle-events.md: afd45027e8b56cbf1d17e6dec749d8602c81124d -2026-07-05-subagent-provider-lifecycle-events.zh.md: 01e12946c74fc4fb33c96e047d2030e99b692f47 +2026-07-05-subagent-provider-lifecycle-events.md: 503c0d638785e3a4944c8903b8d7d469c68b1881 +2026-07-05-subagent-provider-lifecycle-events.zh.md: e7d1d8c398a6d77efb2dbccc365e0040f38f2d33 diff --git a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md index afd45027e8..503c0d6387 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md +++ b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md @@ -31,6 +31,6 @@ The events also complete the seam's vocabulary: `ctx.subagents` is a named regis ## Consequences - Consumers deriving state from a named provider react to `subagent/provider-added`/`-removed` instead of reading the registry at `apply` time; `dsh-tool-subagent` is the reference implementation. -- **Addition fails loud; removal is contained per listener.** An addition listener may unwind registration. Removal runs during disposal, so one throwing listener is logged without starving later mirrors or disrupting teardown. `start()` still resolves the provider by name for every run, preventing stale tools from calling a removed backend. See the [events catalog](../../../../docs/cordis-catalog/events.md) and [producer/consumer map](../../../../docs/event-producer-consumer.md). +- **Addition fails loud; removal is contained per listener.** An addition listener may unwind registration. Removal runs during disposal, so one throwing listener is logged without starving later mirrors or disrupting teardown. `start()` still resolves the provider by name for every run, preventing stale tools from calling a removed backend. See the [events catalog](../../../../docs/subsystems/subagent.md#cordis-surface) and [producer/consumer map](../../../../docs/event-producer-consumer.md). - **A window where the tool is absent.** Between backend disposal and re-registration (an HMR reload), the model sees no subagent tool. This is the honest state — the alternative is a tool that dispatches into nothing — and the tool registry's `tools/change` emit keeps prompt assembly current. - **Two waiting fibers sharing a `toolName` is an invalid config caught late.** If two loads of `dsh-tool-subagent` name different providers but the same `toolName`, both wait, and whichever provider arrives first registers; the second registration throws only when ITS provider arrives. `TODO(subagent-dup-toolname)` in the plugin records this blast radius; the tool registry's duplicate-name rejection remains the backstop. diff --git a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md index 01e12946c7..e7d1d8c398 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md @@ -31,6 +31,6 @@ Status: implemented ## 后果 - 从命名提供方派生状态的消费方响应 `subagent/provider-added`/`-removed` 事件,而非在 `apply` 时读取注册表;`dsh-tool-subagent` 是参考实现。 -- **添加时大声失败;移除时按监听器隔离。** 添加监听器可以回滚注册。移除在 disposal 期间运行,因此单个监听器抛异常只会被记录到日志中,不会阻止后续镜像运行或干扰拆解流程。`start()` 仍在每次运行时按名称解析提供方,防止陈旧工具调用已移除的后端。见[事件目录](../../../../docs/cordis-catalog/events.md)与[生产者/消费方映射](../../../../docs/event-producer-consumer.md)。 +- **添加时大声失败;移除时按监听器隔离。** 添加监听器可以回滚注册。移除在 disposal 期间运行,因此单个监听器抛异常只会被记录日志,不会饿死后续镜像或干扰拆解流程。`start()` 仍在每次运行时按名称解析提供方,防止陈旧工具调用已移除的后端。见[事件目录](../../../../docs/subsystems/subagent.md#cordis-surface)与[生产者/消费方映射](../../../../docs/event-producer-consumer.md)。 - **工具不存在的窗口期。** 在后端 disposal 与重新注册之间(HMR 重载期间),模型看不到 subagent 工具。这是诚实的状态——替代方案是一个向空处分发的工具——工具注册表发出的 `tools/change` 事件会使提示词组装保持最新状态。 - **两个等待中的 fiber 共享同一 `toolName` 是无效配置,被延迟捕获。** 如果两个 `dsh-tool-subagent` 加载实例分别指定了不同的提供方但相同的 `toolName`,两者都会等待,先到达的提供方先注册;第二次注册仅在其提供方到达时才抛异常。插件中的 `TODO(subagent-dup-toolname)` 记录了这一影响范围;工具注册表的重名拒绝机制仍是最终防线。 diff --git a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml index cdee6ab259..215f612f5e 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.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/architecture/2026-07-08-agent-scope-contexts.md -2026-07-08-agent-scope-contexts.md: 5e09bdbcae1e57e6b65eb7d1720a6e7a7f758a9f -2026-07-08-agent-scope-contexts.zh.md: 4714045f28e0386a3a53b53437d063462e75a9f1 +2026-07-08-agent-scope-contexts.md: eb3f6f247bac1a1d81aa2644132c7b9cc04d602c +2026-07-08-agent-scope-contexts.zh.md: 673dd8578bf545b1f14f3b7e7b89874d804e4ae9 diff --git a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md index 5e09bdbcae..eb3f6f247b 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md +++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md @@ -104,7 +104,7 @@ An event about Agent A normally reaches unscoped listeners and A-scoped listener At the Cordis level, `Scoped` is an opaque routing receiver. It carries the filter used to choose listeners but is not the domain object. Event signatures therefore keep the real `Agent`, tool execution, approval request, or other subject as an explicit argument that listeners can inspect. -A listener registered with `{ global: true }` deliberately bypasses contextual audience filtering while its cleanup still follows the registering context. Registry-membership notifications remain unfiltered because they describe shared registry state rather than one agent's operation. The generated [event catalog](../../../../docs/cordis-catalog/events.md) is the exhaustive event reference. +A listener registered with `{ global: true }` deliberately bypasses contextual audience filtering while its cleanup still follows the registering context. Registry-membership notifications remain unfiltered because they describe shared registry state rather than one agent's operation. The exhaustive event reference is the set of generated `cordis-surface` regions across the [subsystem pages](../../../../docs/subsystems/core.md) — each event scope on its owning page (`agent/*` and `agent-loop/*` on core.md itself). ### Creation publishes last and disposal revokes last diff --git a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md index 4714045f28..673dd8578b 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md @@ -104,7 +104,7 @@ setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通插 在 Cordis 层面,`Scoped` 是一个不透明的路由接收器。它携带用于选择监听器的过滤器,但本身不是领域对象。因此事件签名将真实的 `Agent`、工具执行、审批请求或其他主体作为显式参数保留,供监听器检查。 -以 `{ global: true }` 注册的监听器有意绕过上下文受众过滤,但其清理仍跟随注册上下文。注册表成员变更通知保持不过滤,因为它们描述的是共享注册表状态而非某个 agent 的操作。生成的[事件目录](../../../../docs/cordis-catalog/events.md)是详尽的事件参考。 +以 `{ global: true }` 注册的监听器有意绕过上下文受众过滤,但其清理仍跟随注册上下文。注册表成员变更通知保持不过滤,因为它们描述的是共享注册表状态而非某个 agent 的操作。详尽的事件参考是各[子系统页面](../../../../docs/subsystems/core.md)上生成的 `cordis-surface` 区块的集合——每个事件作用域在其所属页面上(`agent/*` 与 `agent-loop/*` 在 core.md 本页)。 ### 创建最后发布,dispose 最后撤销 diff --git a/.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml b/.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml index 6fff14bc85..e0f79bacb0 100644 --- a/.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-05-skill-system.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/feature/2026-07-05-skill-system.md -2026-07-05-skill-system.md: bb7c03a64a3d0d0f700e21bc96633f1c3ea181b9 -2026-07-05-skill-system.zh.md: 8e26e52d3366505a958dd4b19461edf8bf740b00 +2026-07-05-skill-system.md: 5961c535a1397c487b0255f8a27d60929c2eeace +2026-07-05-skill-system.zh.md: 05bd880ecbd46f2b472eb2cc5442f11c07f89b4e diff --git a/.agents/notes/implemented/feature/2026-07-05-skill-system.md b/.agents/notes/implemented/feature/2026-07-05-skill-system.md index bb7c03a64a..5961c535a1 100644 --- a/.agents/notes/implemented/feature/2026-07-05-skill-system.md +++ b/.agents/notes/implemented/feature/2026-07-05-skill-system.md @@ -28,7 +28,7 @@ Local skill filesystem I/O goes through `ctx.fs` when a filesystem service is lo The registry's `list()` returns every winning summary, while model and user consumers apply the invocation predicates owned by the [independent invocation-policy decision](2026-07-28-skill-invocation-policy.md). The `skill({ name })` tool loads one model-invocable skill for the current agent cwd and returns a tool result containing ``, ``, and ``. `resourceBase` supplies a directory, URL, or opaque provider-managed base for explicitly referenced scripts, references, and assets; resources load only as needed, without directory enumeration. An unresolved name reports that the skill is unknown or no longer available; invalid names and skills with `invocation.modelInvocable: false` retain distinct tool errors. The tool result is the model-visible disclosure path. -The data structures and catalog/tool contract are documented in [skills.md](../../../../docs/subsystems/skills.md), with service signatures in the generated [services catalog](../../../../docs/cordis-catalog/services.md). +The data structures and catalog/tool contract are documented in [skills.md](../../../../docs/subsystems/skills.md), with service signatures in the generated [services catalog](../../../../docs/subsystems/skills.md#cordis-surface). ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md b/.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md index 8e26e52d33..05bd880ecb 100644 --- a/.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md +++ b/.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md @@ -28,7 +28,7 @@ DeepSeek Harness 使用同一原语,使项目特定的评审、插件编写和 注册表的 `list()` 返回全部胜出摘要,而模型与用户消费方应用[独立调用策略决策](2026-07-28-skill-invocation-policy.md)定义的调用判定。`skill({ name })` 工具为当前 agent cwd 加载一个模型可调用的 skill,返回包含 ``、`` 和 `` 的工具结果。`resourceBase` 提供一个目录、URL 或不透明的提供方管理的基路径,用于显式引用的脚本、参考资料和资产;资源仅按需加载,不进行目录枚举。无法解析的名称报告该 skill 未知或不再可用;无效名称和 `invocation.modelInvocable` 为 `false` 的 skill 保留不同的工具错误。工具结果是面向模型的可见披露路径。 -数据结构与目录/工具契约记录在 [skills.md](../../../../docs/subsystems/skills.md) 中,服务签名见生成的[服务目录](../../../../docs/cordis-catalog/services.md)。 +数据结构与目录/工具契约记录在 [skills.md](../../../../docs/subsystems/skills.md) 中,服务签名见生成的[服务目录](../../../../docs/subsystems/skills.md#cordis-surface)。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.i18n.yaml b/.agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.i18n.yaml new file mode 100644 index 0000000000..102b0ce5fe --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.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-07-28-per-subsystem-cordis-surface-regions.md +2026-07-28-per-subsystem-cordis-surface-regions.md: ef65bfc4c7dadd7dafe1a38f41656e6ecc61ea50 +2026-07-28-per-subsystem-cordis-surface-regions.zh.md: 1df18d7260800b89c95a9d4aeb0301adb0ee56f6 diff --git a/.agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.md b/.agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.md new file mode 100644 index 0000000000..ef65bfc4c7 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.md @@ -0,0 +1,36 @@ +# Agent Note: Per-subsystem generated cordis-surface regions + +Status: implemented + +English | [中文](2026-07-28-per-subsystem-cordis-surface-regions.zh.md) + +## Problem + +One subsystem's documentation was split across three homes: its hand-written subsystems page (introduction, data structures, verbs), its `ctx.` slice of the flat generated `docs/cordis-catalog/services.md`, and its event scope's slice of the flat `docs/cordis-catalog/events.md`. A reader of bash.md had to open two more documents to see the service interface and events the page was describing, and nothing tied the three views together beyond hand-maintained links. The flat catalogs also sat outside the bilingual corpus (excluded from pairing because generated output is English-only), so the reference surface had no Chinese route at all. + +The [generated-catalog decision](../../archived/process/2026-06-20-generated-cordis-catalog.md) — generate from source, `@mode` tags cross-checked, fail-closed type-link coverage, the `ts cordis-catalog` fence — is not in question; what changed is WHERE the generated output lands. + +## Decision + +`gen-cordis-catalog.ts` injects each subsystem's service and event reference INTO its own page, between `` / `` markers, and the flat services/events catalogs are deleted. One page per subsystem now carries introduction, data structures, and the generated wiring surface. + +- **Curated fail-loud partition.** `SERVICE_PAGE` maps every discovered `ctx.` to exactly one page; `EVENT_SCOPE_PAGE` maps every event scope. The generator hard-errors in both directions — an unmapped discovered service/scope, and a mapped key/scope the walk no longer discovers — so the partition cannot drift from the source surface. An independent scan of EVERY `declare module 'cordis'` Context merge backstops the rendering walk's blind spot (it only sees a root `index.ts` with a same-named service class): a declared key the walk cannot render must carry a named `SERVICE_WALK_EXEMPTIONS` reason (today: the `ctx.agent` DX accessor, plus the interface-typed or non-index-declared lsp/apiProxy/appShell/tuiPrompt/tuiResumeHost), and stale exemptions hard-error; a `TODO(cordis-catalog-interface-services)` marks teaching the walk to render them. +- **Byte-identical regions across the pair.** The generator writes the SAME English region bytes into `foo.md` and `foo.zh.md`, extending the existing rule that verbatim code fences match across a pair. `verify-translation-pairing` gained a dedicated region-identity check (`partitionGeneratedRegions` in `translation-pairing.ts` owns the marker grammar) that names a divergent or malformed region precisely; the whole-document structural signature still covers the region content a second time. +- **Guarded pair auto-record.** A regeneration that changes region bytes would leave every touched pair out-of-sync, so the generator re-records a pair's `.i18n.yaml` itself — but ONLY when the write is region-confined: both sides' recorded blob hashes must match the pre-write bytes, and the region-STRIPPED content must be unchanged on both sides. Human-prose drift leaves the record stale so the pairing gate still forces the normal translation flow; a brand-new pair is never auto-recorded (the author's reviewed `--write` owns that). This keeps `.i18n.yaml` as plain `git hash-object` values — no stripped-hash semantics change. +- **The inherited tier moved, not died.** The vendor `ctx` members and `internal/*`/loader/hmr/timer events render to `docs/cordis-api/inherited.md`, next to the relocated Cordis core API pages (`docs/cordis-catalog/core/` → `docs/cordis-api/`). Framework surface lives under a framework home; the harness pages stay repository-owned vocabulary. +- **In-page links.** Signature `Types:` lines link sibling pages (`core.md`, `bash.md`); a type whose primary page is the rendering page is dropped from the line instead of self-linking. Pages reference their own region with `#cordis-surface` or a `#ctx--` anchor — every generated heading is preceded by an explicit `` carrying the GitHub slug (the historical flat-catalog anchor), so the fragments resolve identically on GitHub and the VitePress site, whose own slugger treats the punctuation-heavy headings differently. + +## Alternatives considered + +- **Keep the flat catalogs alongside the regions, both generated** — rejected: every JSDoc edit would produce double diff noise, and the scattering (one subsystem, three documents) this change exists to remove would survive. +- **Generator-owned whole pages with hand-written intros in fragment files** — rejected: the narrative prose is the majority of every existing page and belongs in the reviewed document itself; markers cost one grammar rule and keep authors editing the real file. +- **Localized regions (generator emits Chinese too)** — deferred, same status as the i18n README's long-standing note for the remaining generated docs: teaching the generator zh output means translating source JSDoc, which is machinery this change does not need. English regions inside zh pages match the existing status quo of English JSDoc inside verbatim fences. +- **Hashing region-stripped content in `.i18n.yaml`** — rejected: the record would stop being `git hash-object` of the file, breaking the recover-last-confirmed-text property and every consumer that recomputes hashes. + +## Consequences + +- A subsystem's whole story is one page: `docs/subsystems/.md` (and its pair) carries introduction, data structures/verbs, and the generated service/event surface; `docs/cordis-catalog/` no longer exists. +- A new service or event scope cannot ship undocumented or unmapped: the generator fails until `SERVICE_PAGE`/`EVENT_SCOPE_PAGE` names its owning page, and the page must already exist with markers in both language sides. +- Regeneration after a source-JSDoc change touches the affected pages in both languages plus (when region-confined) their pair records — a mechanical, reviewable diff; prose edits keep demanding the translation flow because the auto-record guard refuses them. +- The website's subsystem nav lists every page (38 routes per locale: 35 translated pairs plus the three still-English-mirrored goal/pty/commands), replacing the two flat catalog nav entries; the Cordis API section gained `inherited.md`. +- `packages/typert/generator/tests/cordis-catalog-contract.spec.ts` pins the region renderer (`renderPageRegion`), the same-page link-drop rule, and the fail-loud JSDoc/type-link validation; `scripts/translation-pairing.spec.ts` pins the marker grammar and blob-hash primitive; `scripts/gen-cordis-catalog-record.spec.ts` proves the auto-record guard refuses every invalid state (stale record, malformed or renamed-key sidecar, extra entries, prose drift, missing record, missing snapshot). diff --git a/.agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.zh.md b/.agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.zh.md new file mode 100644 index 0000000000..1df18d7260 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.zh.md @@ -0,0 +1,36 @@ +# Agent Note: 按子系统生成的 cordis-surface 区块 + +Status: implemented + +[English](2026-07-28-per-subsystem-cordis-surface-regions.md) | 中文 + +## 问题 + +一个子系统的文档过去分散在三个归属:手写的 subsystems 页面(介绍、数据结构、动词)、平铺生成的 `docs/cordis-catalog/services.md` 中属于它的 `ctx.` 切片,以及平铺的 `docs/cordis-catalog/events.md` 中属于其事件作用域的切片。bash.md 的读者必须再打开两份文档,才能看到该页面正在描述的服务接口与事件;除了手工维护的链接,没有任何机制把这三个视图联系在一起。平铺目录还游离在双语语料之外(生成输出只有英文,故被排除在配对之外),于是这个参考面完全没有中文入口。 + +[生成式目录决策](../../archived/process/2026-06-20-generated-cordis-catalog.md)本身(从源码生成、`@mode` 标签交叉校验、失败关闭的类型链接覆盖、`ts cordis-catalog` 围栏)不在质疑之列;改变的只是生成输出「落在哪里」。 + +## 决策 + +`gen-cordis-catalog.ts` 把每个子系统的服务与事件参考注入到该子系统自己的页面内部,置于 `` / `` 标记之间;平铺的 services/events 目录随之删除。现在每个子系统由一个页面同时承载介绍、数据结构和生成的接线表面。 + +- **人工维护、响亮失败的划分。** `SERVICE_PAGE` 把发现的每个 `ctx.` 映射到恰好一个页面;`EVENT_SCOPE_PAGE` 映射每个事件作用域。生成器在两个方向上都会直接报错(既有被发现却未映射的服务或作用域,也有已映射但遍历不再发现的键或作用域),因此划分不可能相对源码表面发生漂移。一个独立扫描会读取每一处 `declare module 'cordis'` 的 Context 合并,为渲染遍历的盲区(它只看得到根 `index.ts` 中同名服务类)兜底:遍历渲染不了的已声明键必须在 `SERVICE_WALK_EXEMPTIONS` 中带着点名理由(目前是 `ctx.agent` 这一 DX 访问器字段,加上接口类型或不在 index 声明的 lsp/apiProxy/appShell/tuiPrompt/tuiResumeHost),陈旧豁免同样直接报错;教会遍历渲染它们的后续工作由 `TODO(cordis-catalog-interface-services)` 标记。 +- **区块在配对两侧按字节一致。** 生成器把同一份英文区块字节写入 `foo.md` 和 `foo.zh.md`,是对「围栏代码块在配对两侧逐字节一致」这一既有规则的延伸。`verify-translation-pairing` 新增了专门的区块一致性检查(标记语法归 `translation-pairing.ts` 中的 `partitionGeneratedRegions` 所有),能精确点名出现分歧或格式错误的区块;整篇文档的结构签名仍会把区块内容再覆盖一遍。 +- **带防护的配对自动记录。** 一次改变区块字节的重新生成会让每个被触及的配对失去同步,因此生成器会自行重新记录配对的 `.i18n.yaml`,但仅限本次写入完全限定在区块内的情况:两侧记录的 blob hash 必须与写入前的字节相符,且两侧剥离区块后的内容必须没有变化。人工行文若有漂移,记录就保持陈旧,配对门禁因此仍会强制走正常翻译流程;全新的配对绝不自动记录(那归作者经评审的 `--write` 所有)。这样 `.i18n.yaml` 保持为纯粹的 `git hash-object` 值:不引入任何「剥离后 hash」的语义变化。 +- **继承层搬了家,而非消亡。** vendor 的 `ctx` 成员与 `internal/*`/loader/hmr/timer 事件渲染到 `docs/cordis-api/inherited.md`,紧邻迁移后的 Cordis 核心 API 页面(`docs/cordis-catalog/core/` → `docs/cordis-api/`)。框架表面落在框架自己的归属之下;harness 页面仍是仓库自有的词汇。 +- **页内链接。** 签名的 `Types:` 行链接到兄弟页面(`core.md`、`bash.md`);若某个类型的主要页面就是正在渲染的页面,该类型会从该行去掉,而不是链接到自身。页面用 `#cordis-surface` 或 `#ctx--` 锚点引用自己的区块:每个生成标题前都有一个显式 ``,携带 GitHub slug(即平铺目录时期的历史锚点),因此这些片段在 GitHub 与 VitePress 站点上解析一致——后者自带的 slugger 对含大量标点的标题会得出不同结果。 + +## 曾考虑的替代方案 + +- **平铺目录与区块并存、两者都生成**:否决。每次 JSDoc 编辑都会产生双份 diff 噪音,而本次变更本要消除的分散状况(一个子系统、三份文档)也将延续。 +- **整页归生成器所有、手写介绍放进片段文件**:否决。叙述性行文占每个现有页面的大部分,应当留在被评审的文档本身;标记只花一条语法规则的成本,还让作者继续编辑真实文件。 +- **本地化区块(生成器同时输出中文)**:推迟,与 i18n README 中针对其余生成文档的长期备注同属一个状态:教会生成器输出中文意味着要翻译源码 JSDoc,而那是本次变更并不需要的机制。zh 页面里的英文区块,与「英文 JSDoc 出现在逐字节一致的围栏代码块内」这一既有现状相符。 +- **在 `.i18n.yaml` 中对剥离区块后的内容做 hash**:否决。记录将不再是文件的 `git hash-object`,这会破坏「还原上次确认文本」的性质,也会破坏每个自行重算 hash 的消费方。 + +## 后果 + +- 一个子系统的完整故事就在一个页面上:`docs/subsystems/.md`(及其配对文件)承载介绍、数据结构/动词,以及生成的服务/事件表面;`docs/cordis-catalog/` 不复存在。 +- 新的服务或事件作用域无法在未记录、未映射的状态下落地:在 `SERVICE_PAGE`/`EVENT_SCOPE_PAGE` 点名其所属页面之前,生成器一直失败,而且该页面必须已经存在,并在两个语言侧都带有标记。 +- 源码 JSDoc 变更后的重新生成会触及两种语言的受影响页面,外加(当写入限定在区块内时)它们的配对记录:一份机械、可评审的 diff。行文编辑仍然要走翻译流程,因为自动记录防护会拒绝它们。 +- 网站的子系统导航列出每个页面(每个 locale 38 条路由:35 个已翻译配对,加上仍为英文镜像的 goal/pty/commands 三页),取代两个平铺目录导航项;Cordis API 一节新增 `inherited.md`。 +- `packages/typert/generator/tests/cordis-catalog-contract.spec.ts` 固定区块渲染器(`renderPageRegion`)、同页链接去除规则,以及响亮失败的 JSDoc 与类型链接校验;`scripts/translation-pairing.spec.ts` 固定标记语法与 blob hash 原语;`scripts/gen-cordis-catalog-record.spec.ts` 证明自动重录守卫拒绝每一种非法状态(陈旧记录、格式错误或键被改名的伴随记录、多余条目、行文漂移、记录缺失、快照缺失)。 diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 488a7a1dd6..5194544f95 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -28,7 +28,7 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home. | [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history | | Package README | Per-package config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc or catalog restatement, other packages' concerns | | [development.md](development.md) | Contributor onboarding: setup, daily workflow, and CI shape at summary level | Runtime rationale (→ Agent Notes), drifting gate inventories | -| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [Cordis core API](cordis-catalog/core/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind | +| Generated reference: the per-page `cordis-surface` regions in [subsystems/](subsystems/core.md), the [Cordis core API + inherited tier](cordis-api/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind (regions included) | | Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) | ## Writing rules diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 15a6ba3fb5..2436ff9027 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.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/architecture.md -architecture.md: 7baf550128fe83528ce12bf2f9b40cfc8f78e322 -architecture.zh.md: 076edd0549fb7f648a7a1b06d7027a1e6eca735f +architecture.md: f593ee5c54e5bd2cbdaa5da9f2de54b7b08ec14b +architecture.zh.md: 5780d54aad6085ef7811dfa94377a4c2478bfb99 diff --git a/docs/architecture.md b/docs/architecture.md index 7baf550128..f593ee5c54 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -53,7 +53,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services, ## Event -Events are the service extension API ([catalog](cordis-catalog/events.md), [producer/consumer map](event-producer-consumer.md)). +Events are the service extension API ([subsystems](subsystems/core.md), [producer/consumer map](event-producer-consumer.md)). ### Event Domains diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 076edd0549..5780d54aad 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -53,7 +53,7 @@ ## 事件 -事件就是服务的扩展 API([目录](cordis-catalog/events.md)、[生产方与消费方映射](event-producer-consumer.md))。 +事件就是服务的扩展 API([子系统](subsystems/core.md)、[生产方与消费方映射](event-producer-consumer.md))。 ### 事件域 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 850f4731e2..eb4472d629 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3,7 +3,7 @@ # Plugin Config Catalog -Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin's full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the cordis [events](cordis-catalog/events.md) + [services](cordis-catalog/services.md) catalogs, the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference. +Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin's full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the generated `cordis-surface` region on each [subsystem page](subsystems/core.md), the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference. This file is GENERATED from source (`scripts/gen-config-catalog.ts`) and verified fresh by `pnpm run verify-config-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks use a `ts config-catalog` fence (skipped by doc-typecheck, since a lone declaration referencing imports is not standalone-compilable). The generator also cross-checks the runtime schemastery schema against the pasted declaration — every schema-validated key, nested keys included, must be locatable on the declared config type — so the paste cannot hide a loader-accepted field. diff --git a/docs/cordis-catalog/core/context.md b/docs/cordis-api/context.md similarity index 91% rename from docs/cordis-catalog/core/context.md rename to docs/cordis-api/context.md index c0c1690c8f..320b23de1c 100644 --- a/docs/cordis-catalog/core/context.md +++ b/docs/cordis-api/context.md @@ -9,7 +9,7 @@ Root and child dependency containers for Cordis plugins. A context is a proxy: normal property reads go through the service resolver, while `extend()`, `isolate()`, and `intercept()` create scoped child contexts without mutating their parent. -[Source](../../../vendor/cordis/src/context.ts#L42) +[Source](../../vendor/cordis/src/context.ts#L42) ### ctx.extend(meta?) @@ -34,7 +34,7 @@ The child prototypally inherits every property of this context; own properties o **Returns** a child context inheriting from this one. -[Source](../../../vendor/cordis/src/context.ts#L99) +[Source](../../vendor/cordis/src/context.ts#L99) ### ctx.isolate(name, label?) @@ -63,7 +63,7 @@ Below the returned context, reads and writes of the service `name` resolve again **Returns** a child context whose `name` service resolves in the new scope. -[Source](../../../vendor/cordis/src/context.ts#L121) +[Source](../../vendor/cordis/src/context.ts#L121) ### ctx.intercept(name, config) @@ -93,7 +93,7 @@ Plugins loaded under the returned context see `config` merged into the service's **Returns** a child context carrying the additional intercept entry. -[Source](../../../vendor/cordis/src/context.ts#L139) +[Source](../../vendor/cordis/src/context.ts#L139) ### ctx.root @@ -104,7 +104,7 @@ root: this The root context of the application (every child context shares it). @experimental -[Source](../../../vendor/cordis/src/context.ts#L22) +[Source](../../vendor/cordis/src/context.ts#L22) ### ctx.baseUrl @@ -115,7 +115,7 @@ baseUrl?: string Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. -[Source](../../../vendor/cordis/src/context.ts#L24) +[Source](../../vendor/cordis/src/context.ts#L24) ### ctx.events @@ -126,7 +126,7 @@ events: EventsService The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). -[Source](../../../vendor/cordis/src/context.ts#L26) +[Source](../../vendor/cordis/src/context.ts#L26) ### ctx.logger @@ -137,7 +137,7 @@ logger: LoggerService The logging service. Call `ctx.logger(name)` for a named logger. -[Source](../../../vendor/cordis/src/context.ts#L28) +[Source](../../vendor/cordis/src/context.ts#L28) ### ctx.reflect @@ -148,7 +148,7 @@ reflect: ReflectService The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). -[Source](../../../vendor/cordis/src/context.ts#L30) +[Source](../../vendor/cordis/src/context.ts#L30) ### ctx.registry @@ -159,7 +159,7 @@ registry: RegistryService The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). -[Source](../../../vendor/cordis/src/context.ts#L32) +[Source](../../vendor/cordis/src/context.ts#L32) ## Static members @@ -172,7 +172,7 @@ static readonly effect: unique symbol Symbol key under which a disposer exposes its EffectMeta diagnostics tree. -[Source](../../../vendor/cordis/src/context.ts#L44) +[Source](../../vendor/cordis/src/context.ts#L44) ### Context.filter @@ -183,7 +183,7 @@ static readonly filter: unique symbol Symbol key for a context's listener filter, consulted on every event dispatch. -[Source](../../../vendor/cordis/src/context.ts#L46) +[Source](../../vendor/cordis/src/context.ts#L46) ### Context.isolate @@ -194,7 +194,7 @@ static readonly isolate: unique symbol Symbol key of the isolation map (see the `Context[symbols.isolate]` property). -[Source](../../../vendor/cordis/src/context.ts#L48) +[Source](../../vendor/cordis/src/context.ts#L48) ### Context.intercept @@ -205,7 +205,7 @@ static readonly intercept: unique symbol Symbol key of the intercept map (see the `Context[symbols.intercept]` property). -[Source](../../../vendor/cordis/src/context.ts#L50) +[Source](../../vendor/cordis/src/context.ts#L50) ### Context.is(value) @@ -230,7 +230,7 @@ Works across realms and across multiple copies of cordis, because the brand is k **Returns** `true` if `value` is a Cordis context, narrowing its type. -[Source](../../../vendor/cordis/src/context.ts#L61) +[Source](../../vendor/cordis/src/context.ts#L61) ## Service store and mixins @@ -256,7 +256,7 @@ Read a service from the store without the inject requirement. **Returns** the service value, or `undefined` when not (yet) provided. -[Source](../../../vendor/cordis/src/reflect.ts#L17) +[Source](../../vendor/cordis/src/reflect.ts#L17) ### ctx.set(name, value) @@ -281,7 +281,7 @@ Only the fiber that provided the service may set it; setting an unprovided name - `name` — the service name. - `value` — the new service value. -[Source](../../../vendor/cordis/src/reflect.ts#L29) +[Source](../../vendor/cordis/src/reflect.ts#L29) ### ctx.provide(name, value) @@ -311,7 +311,7 @@ The service becomes visible to dependents in the same isolation scope once the f **Returns** a disposer that unregisters the service. -[Source](../../../vendor/cordis/src/reflect.ts#L44) +[Source](../../vendor/cordis/src/reflect.ts#L44) ### ctx.accessor(name, options) @@ -335,7 +335,7 @@ The accessor is removed when the current fiber unloads. Throws if the name is al - `name` — the context property name. - `options` — the `get` hook and optional `set` hook. -[Source](../../../vendor/cordis/src/reflect.ts#L56) +[Source](../../vendor/cordis/src/reflect.ts#L56) ### ctx.mixin(name, mixins) @@ -361,4 +361,4 @@ Each mixed-in key becomes an accessor that forwards to the service (binding meth - `name` — the context property holding the source service. - `mixins` — keys to forward, or a source-key → ctx-key map. -[Source](../../../vendor/cordis/src/reflect.ts#L67) +[Source](../../vendor/cordis/src/reflect.ts#L67) diff --git a/docs/cordis-catalog/core/events.md b/docs/cordis-api/events.md similarity index 92% rename from docs/cordis-catalog/core/events.md rename to docs/cordis-api/events.md index 68cd109e8e..8b8eb2358e 100644 --- a/docs/cordis-catalog/core/events.md +++ b/docs/cordis-api/events.md @@ -3,7 +3,7 @@ # Events -The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../events.md). +The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated into each owning [subsystem page](../subsystems/core.md). ### ctx.parallel(name, ...args) @@ -26,7 +26,7 @@ Dispatch an event, running all listeners concurrently. **Returns** a promise resolving once every listener has settled. -[Source](../../../vendor/cordis/src/events.ts#L44) +[Source](../../vendor/cordis/src/events.ts#L44) ### ctx.emit(name, ...args) @@ -46,7 +46,7 @@ Dispatch an event synchronously, ignoring listener return values. - `name` — the event name. - `args` — arguments passed to every listener. -[Source](../../../vendor/cordis/src/events.ts#L53) +[Source](../../vendor/cordis/src/events.ts#L53) ### ctx.serial(name, ...args) @@ -69,7 +69,7 @@ Dispatch an event, awaiting listeners in order until one bails. **Returns** the first bail value (non-null, non-false, non-undefined), if any. -[Source](../../../vendor/cordis/src/events.ts#L63) +[Source](../../vendor/cordis/src/events.ts#L63) ### ctx.bail(name, ...args) @@ -92,7 +92,7 @@ Dispatch an event, calling listeners in order until one bails. **Returns** the first bail value (non-null, non-false, non-undefined), if any. -[Source](../../../vendor/cordis/src/events.ts#L73) +[Source](../../vendor/cordis/src/events.ts#L73) ### ctx.waterfall(name, ...args) @@ -120,7 +120,7 @@ Each listener wraps the rest of the chain: calling `next()` invokes the next lis **Returns** the outermost listener's return value. -[Source](../../../vendor/cordis/src/events.ts#L86) +[Source](../../vendor/cordis/src/events.ts#L86) ### ctx.on(name, listener, options?) @@ -144,7 +144,7 @@ Register an event listener owned by the current fiber. **Returns** a disposer removing the listener; `true` if it was still registered. -[Source](../../../vendor/cordis/src/events.ts#L97) +[Source](../../vendor/cordis/src/events.ts#L97) ### ctx.once(name, listener, options?) @@ -168,7 +168,7 @@ Same as `on()`, but the listener disposes itself after its first call. **Returns** a disposer removing the listener; `true` if it was still registered. -[Source](../../../vendor/cordis/src/events.ts#L106) +[Source](../../vendor/cordis/src/events.ts#L106) ## EventOptions @@ -184,7 +184,7 @@ interface EventOptions { } ``` -[Source](../../../vendor/cordis/src/events.ts#L112) +[Source](../../vendor/cordis/src/events.ts#L112) ## DispatchMode @@ -204,4 +204,4 @@ Event dispatch strategy used by the event service. type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall' ``` -[Source](../../../vendor/cordis/src/events.ts#L32) +[Source](../../vendor/cordis/src/events.ts#L32) diff --git a/docs/cordis-catalog/core/fiber.md b/docs/cordis-api/fiber.md similarity index 90% rename from docs/cordis-catalog/core/fiber.md rename to docs/cordis-api/fiber.md index 35a991f789..36d2861ac6 100644 --- a/docs/cordis-catalog/core/fiber.md +++ b/docs/cordis-api/fiber.md @@ -34,7 +34,7 @@ Register a cleanup-aware effect on this fiber. **Returns** a disposer that tears the effect down and settles once done. -[Source](../../../vendor/cordis/src/fiber.ts#L420) +[Source](../../vendor/cordis/src/fiber.ts#L420) ### ctx.fiber @@ -45,7 +45,7 @@ fiber: Fiber The fiber (plugin runtime instance) that owns this context. -[Source](../../../vendor/cordis/src/fiber.ts#L12) +[Source](../../vendor/cordis/src/fiber.ts#L12) ## The Fiber class @@ -53,7 +53,7 @@ Runtime instance of one plugin application. A fiber tracks dependency state, validated config, lifecycle effects, and cleanup for the plugin context returned by `ctx.plugin()`. -[Source](../../../vendor/cordis/src/fiber.ts#L184) +[Source](../../vendor/cordis/src/fiber.ts#L184) ### fiber.uid @@ -64,7 +64,7 @@ public uid: number | null Unique id within the registry; 0 for the root fiber, `null` once disposed. -[Source](../../../vendor/cordis/src/fiber.ts#L186) +[Source](../../vendor/cordis/src/fiber.ts#L186) ### fiber.ctx @@ -75,7 +75,7 @@ public readonly ctx: Context The context this fiber's plugin runs in (extends the parent context). -[Source](../../../vendor/cordis/src/fiber.ts#L188) +[Source](../../vendor/cordis/src/fiber.ts#L188) ### fiber.config @@ -86,7 +86,7 @@ public config: any The validated plugin config (updated by `update()`). -[Source](../../../vendor/cordis/src/fiber.ts#L190) +[Source](../../vendor/cordis/src/fiber.ts#L190) ### fiber.state @@ -97,7 +97,7 @@ public state Current lifecycle state; transitions emit `internal/status`. -[Source](../../../vendor/cordis/src/fiber.ts#L192) +[Source](../../vendor/cordis/src/fiber.ts#L192) ### fiber.dispose @@ -108,7 +108,7 @@ public readonly dispose: () => Promise Dispose this fiber: unload the plugin, then settle once cleanup finished. -[Source](../../../vendor/cordis/src/fiber.ts#L194) +[Source](../../vendor/cordis/src/fiber.ts#L194) ### fiber.store @@ -119,7 +119,7 @@ public store: Dict | undefined Snapshot of required service implementations while loaded; `undefined` otherwise. -[Source](../../../vendor/cordis/src/fiber.ts#L196) +[Source](../../vendor/cordis/src/fiber.ts#L196) ### fiber.inertia @@ -130,7 +130,7 @@ public inertia: Promise | undefined The in-flight load/unload transition, if one is currently running. -[Source](../../../vendor/cordis/src/fiber.ts#L198) +[Source](../../vendor/cordis/src/fiber.ts#L198) ### fiber.name @@ -141,7 +141,7 @@ get name() The plugin's display name, inherited from the nearest named ancestor, else `'root'`. -[Source](../../../vendor/cordis/src/fiber.ts#L341) +[Source](../../vendor/cordis/src/fiber.ts#L341) ### fiber.assertActive() @@ -159,7 +159,7 @@ Throw if the fiber has already been disposed. **Returns** nothing when the fiber is still active. -[Source](../../../vendor/cordis/src/fiber.ts#L356) +[Source](../../vendor/cordis/src/fiber.ts#L356) ### fiber.effect(execute, label?) @@ -190,7 +190,7 @@ Register a cleanup-aware effect on this fiber. **Returns** a disposer that tears the effect down and settles once done. -[Source](../../../vendor/cordis/src/fiber.ts#L420) +[Source](../../vendor/cordis/src/fiber.ts#L420) ### fiber.getEffects() @@ -207,7 +207,7 @@ Return metadata for currently registered effects. **Returns** one `EffectMeta` tree per labeled live effect. -[Source](../../../vendor/cordis/src/fiber.ts#L573) +[Source](../../vendor/cordis/src/fiber.ts#L573) ### fiber.await() @@ -225,7 +225,7 @@ Wait for current lifecycle work and rethrow startup errors. **Returns** this fiber, once it has settled into a stable state. -[Source](../../../vendor/cordis/src/fiber.ts#L702) +[Source](../../vendor/cordis/src/fiber.ts#L702) ### fiber.restart() @@ -243,7 +243,7 @@ Dispose and immediately reload this plugin with its current config. **Returns** a promise resolving once the reload settled. -[Source](../../../vendor/cordis/src/fiber.ts#L716) +[Source](../../vendor/cordis/src/fiber.ts#L716) ### fiber.update(config, noSave?) @@ -271,7 +271,7 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o **Returns** the update waterfall result; the default restart returns a promise. -[Source](../../../vendor/cordis/src/fiber.ts#L734) +[Source](../../vendor/cordis/src/fiber.ts#L734) ## Effect @@ -292,7 +292,7 @@ type Effect = | AsyncEffect ``` -[Source](../../../vendor/cordis/src/fiber.ts#L83) +[Source](../../vendor/cordis/src/fiber.ts#L83) ## Disposable @@ -310,7 +310,7 @@ Disposers run in reverse registration order when the owning fiber unloads; they type Disposable = () => T ``` -[Source](../../../vendor/cordis/src/fiber.ts#L74) +[Source](../../vendor/cordis/src/fiber.ts#L74) ## EffectMeta @@ -326,7 +326,7 @@ interface EffectMeta { } ``` -[Source](../../../vendor/cordis/src/fiber.ts#L96) +[Source](../../vendor/cordis/src/fiber.ts#L96) ## CordisError @@ -352,7 +352,7 @@ namespace CordisError { } ``` -[Source](../../../vendor/cordis/src/fiber.ts#L157) +[Source](../../vendor/cordis/src/fiber.ts#L157) ## ValidationError @@ -372,4 +372,4 @@ class ValidationError extends TypeError { } ``` -[Source](../../../vendor/cordis/src/fiber.ts#L19) +[Source](../../vendor/cordis/src/fiber.ts#L19) diff --git a/docs/cordis-api/inherited.md b/docs/cordis-api/inherited.md new file mode 100644 index 0000000000..5582bdd1ac --- /dev/null +++ b/docs/cordis-api/inherited.md @@ -0,0 +1,39 @@ + + +# Inherited Cordis Surface + +The framework `ctx` members and events every plugin sees beyond the harness tier — pinned vendor source ([vendoring policy](../../vendor/README.md)), summarized tersely so the harness pages stay focused on repository-owned vocabulary. Detailed Context, Fiber, Registry, and Service APIs are generated in [context.md](context.md), [fiber.md](fiber.md), [registry.md](registry.md), and [service.md](service.md); the event-dispatch methods in [events.md](events.md). + +This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them. + +## Inherited `ctx` members (cordis core + loader/hmr/timer) + +- `ctx.on / ctx.once` — Register an event listener (disposable). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts)) +- `ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall` — Dispatch an event (sync / awaited / first-bail / veto-chain). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts)) +- `ctx.plugin / ctx.inject` — Load a plugin / declare required services. ([`vendor/cordis/src/registry.ts:164`](../../vendor/cordis/src/registry.ts)) +- `ctx.effect` — Register a disposable side effect tied to the fiber. ([`vendor/cordis/src/fiber.ts:9`](../../vendor/cordis/src/fiber.ts)) +- `ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin` — Low-level service-store access and binding. ([`vendor/cordis/src/reflect.ts:7`](../../vendor/cordis/src/reflect.ts)) +- `ctx.extend / ctx.isolate / ctx.intercept` — Derive a child context (scoped services / isolation / interception). ([`vendor/cordis/src/context.ts:42`](../../vendor/cordis/src/context.ts)) +- `ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger` — Ambient handles onto the running context graph. ([`vendor/cordis/src/context.ts:16`](../../vendor/cordis/src/context.ts)) +- `ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)` — Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick). ([`vendor/timer/src/index.ts:4`](../../vendor/timer/src/index.ts)) +- `ctx.loader` — The config Loader that booted the app (present under the loader). ([`vendor/loader/src/index.ts:30`](../../vendor/loader/src/index.ts)) +- `ctx.hmr` — The hot-module-reload watcher (present under the hmr plugin). ([`vendor/hmr/src/index.ts:15`](../../vendor/hmr/src/index.ts)) + +## Inherited events (cordis core + loader/hmr/timer) + +- `internal/plugin` — A plugin fiber was created. ([`vendor/cordis/src/events.ts:328`](../../vendor/cordis/src/events.ts)) +- `internal/status` — A fiber changed lifecycle state. ([`vendor/cordis/src/events.ts:330`](../../vendor/cordis/src/events.ts)) +- `internal/service` — Interception hook for a service binding (no core producer). ([`vendor/cordis/src/events.ts:332`](../../vendor/cordis/src/events.ts)) +- `internal/update` — Waterfall: a fiber config update is being applied. ([`vendor/cordis/src/events.ts:334`](../../vendor/cordis/src/events.ts)) +- `internal/get` — Waterfall: a service is being read from the store. ([`vendor/cordis/src/events.ts:336`](../../vendor/cordis/src/events.ts)) +- `internal/set` — Waterfall: a service is being written to the store. ([`vendor/cordis/src/events.ts:338`](../../vendor/cordis/src/events.ts)) +- `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:340`](../../vendor/cordis/src/events.ts)) +- `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:342`](../../vendor/cordis/src/events.ts)) +- `hmr/change` — A watched source file changed on disk. ([`vendor/hmr/src/index.ts:20`](../../vendor/hmr/src/index.ts)) +- `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:21`](../../vendor/hmr/src/index.ts)) +- `exit` — The process is exiting on a signal. ([`vendor/loader/src/index.ts:23`](../../vendor/loader/src/index.ts)) +- `loader/config-update` — The loader config tree changed. ([`vendor/loader/src/index.ts:24`](../../vendor/loader/src/index.ts)) +- `loader/entry-init` — A config entry is being initialized. ([`vendor/loader/src/index.ts:25`](../../vendor/loader/src/index.ts)) +- `loader/partial-dispose` — An entry is being partially disposed on reload. ([`vendor/loader/src/index.ts:26`](../../vendor/loader/src/index.ts)) +- `loader/patch-context` — A context is being patched during a reload. ([`vendor/loader/src/index.ts:27`](../../vendor/loader/src/index.ts)) diff --git a/docs/cordis-catalog/core/registry.md b/docs/cordis-api/registry.md similarity index 95% rename from docs/cordis-catalog/core/registry.md rename to docs/cordis-api/registry.md index 6b2e298c72..e6ddce5700 100644 --- a/docs/cordis-catalog/core/registry.md +++ b/docs/cordis-api/registry.md @@ -30,7 +30,7 @@ Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloade **Returns** the fiber; awaiting it settles once loading finished. -[Source](../../../vendor/cordis/src/registry.ts#L176) +[Source](../../vendor/cordis/src/registry.ts#L176) ### ctx.plugin(plugin, ...args) @@ -53,7 +53,7 @@ Load a plugin in the current context. **Returns** the fiber; awaiting it settles once loading finished (rejecting on config or startup errors). -[Source](../../../vendor/cordis/src/registry.ts#L185) +[Source](../../vendor/cordis/src/registry.ts#L185) ## Plugin @@ -118,7 +118,7 @@ namespace Plugin { } ``` -[Source](../../../vendor/cordis/src/registry.ts#L92) +[Source](../../vendor/cordis/src/registry.ts#L92) ## Inject @@ -149,4 +149,4 @@ namespace Inject { } ``` -[Source](../../../vendor/cordis/src/registry.ts#L19) +[Source](../../vendor/cordis/src/registry.ts#L19) diff --git a/docs/cordis-catalog/core/service.md b/docs/cordis-api/service.md similarity index 82% rename from docs/cordis-catalog/core/service.md rename to docs/cordis-api/service.md index 84b74f98df..d4b0f06da1 100644 --- a/docs/cordis-catalog/core/service.md +++ b/docs/cordis-api/service.md @@ -9,7 +9,7 @@ Base class for services that expose a named API on `ctx`. Subclasses call `super(ctx, name)` from their constructor. The service is registered immediately and is automatically removed with the owning fiber. -[Source](../../../vendor/cordis/src/service.ts#L11) +[Source](../../vendor/cordis/src/service.ts#L11) ### service.name @@ -20,7 +20,7 @@ public name!: string The service name this instance is registered under. -[Source](../../../vendor/cordis/src/service.ts#L30) +[Source](../../vendor/cordis/src/service.ts#L30) ## Static members @@ -33,7 +33,7 @@ static readonly init: unique symbol Symbol key of an instance method run after construction (class plugins). -[Source](../../../vendor/cordis/src/service.ts#L13) +[Source](../../vendor/cordis/src/service.ts#L13) ### Service.check @@ -44,7 +44,7 @@ static readonly check: unique symbol Symbol key of the availability predicate passed to `ctx.provide()`. -[Source](../../../vendor/cordis/src/service.ts#L15) +[Source](../../vendor/cordis/src/service.ts#L15) ### Service.config @@ -55,7 +55,7 @@ static readonly config: unique symbol Symbol key of the phantom intercept-config type parameter. -[Source](../../../vendor/cordis/src/service.ts#L17) +[Source](../../vendor/cordis/src/service.ts#L17) ### Service.invoke @@ -66,7 +66,7 @@ static readonly invoke: unique symbol Symbol key of the call body making a service callable (e.g. `ctx.logger()`). -[Source](../../../vendor/cordis/src/service.ts#L19) +[Source](../../vendor/cordis/src/service.ts#L19) ### Service.extend @@ -77,7 +77,7 @@ static readonly extend: unique symbol Symbol key of the helper deriving an extended service instance. -[Source](../../../vendor/cordis/src/service.ts#L21) +[Source](../../vendor/cordis/src/service.ts#L21) ### Service.tracker @@ -88,7 +88,7 @@ static readonly tracker: unique symbol Symbol key of the tracker metadata used for context tracing. -[Source](../../../vendor/cordis/src/service.ts#L23) +[Source](../../vendor/cordis/src/service.ts#L23) ### Service.resolveConfig @@ -99,4 +99,4 @@ static readonly resolveConfig: unique symbol Symbol key of the intercept-config resolution helper below. -[Source](../../../vendor/cordis/src/service.ts#L25) +[Source](../../vendor/cordis/src/service.ts#L25) diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md deleted file mode 100644 index fb05eaa8e6..0000000000 --- a/docs/cordis-catalog/events.md +++ /dev/null @@ -1,1100 +0,0 @@ - - -# Cordis Events Catalog - -Every cordis event a plugin can listen to: exact signature, dispatch mode, and original declaration JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [subsystems/](../subsystems/core.md) catalogs the *data structures* these signatures move around. - -This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them. - -The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md). - -Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`). - -## `agent/*` - -### `agent/created` — emit - -A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving seam. 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. - -```ts cordis-catalog -/** - * A fully configured agent and live session were published. Setup is - * composition-only; `agent/session-start` is the first startup-driving seam. - * 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 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, payload: { agent: Agent }): void -``` - -Types: [Agent](../subsystems/core.md) · [Scoped](../subsystems/scope.md) - -Source: [`packages/core/agent/src/types.ts:158`](../../packages/core/agent/src/types.ts) - -### `agent/disposed` — emit - -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. - -```ts cordis-catalog -/** - * 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 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, payload: { agent: Agent }): void -``` - -Types: [Agent](../subsystems/core.md) · [Scoped](../subsystems/scope.md) - -Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/types.ts) - -### `agent/error` — emit - -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 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, payload: { agent: Agent; turn: number; step: number; error: unknown }): void -``` - -Types: [Agent](../subsystems/core.md) · [Scoped](../subsystems/scope.md) - -Source: [`packages/core/agent/src/types.ts:289`](../../packages/core/agent/src/types.ts) - -### `agent/inbox/claimed` — emit - -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 -/** - * 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/claimed'(this: Scoped, payload: { agent: Agent; message: UserMessage; turn: number }): void -``` - -Types: [Agent](../subsystems/core.md) · [Scoped](../subsystems/scope.md) · [UserMessage](../subsystems/session.md) - -Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts) - -### `agent/inbox/discarded` — emit - -One message was discarded from the live inbox. - -```ts cordis-catalog -/** - * 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/discarded'(this: Scoped, payload: { agent: Agent; message: UserMessage }): void -``` - -Types: [Agent](../subsystems/core.md) · [Scoped](../subsystems/scope.md) · [UserMessage](../subsystems/session.md) - -Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts) - -### `agent/inbox/inserted` — emit - -One message entered the live inbox. - -```ts cordis-catalog -/** - * 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/inserted'(this: Scoped, payload: { agent: Agent; message: UserMessage }): void -``` - -Types: [Agent](../subsystems/core.md) · [Scoped](../subsystems/scope.md) · [UserMessage](../subsystems/session.md) - -Source: [`packages/core/agent/src/types.ts:185`](../../packages/core/agent/src/types.ts) - -### `agent/pre-step` — waterfall - -Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages. - -```ts cordis-catalog -/** - * 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/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise -``` - -Types: [Agent](../subsystems/core.md) · [PreStepDecision](../subsystems/core.md) · [Scoped](../subsystems/scope.md) · [UserMessage](../subsystems/session.md) - -Source: [`packages/core/agent/src/types.ts:230`](../../packages/core/agent/src/types.ts) - -### `agent/request` — waterfall - -Replace the frozen call configuration. `await next()` yields the config 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. - -```ts cordis-catalog -/** - * Replace the frozen call configuration. `await next()` yields the config - * 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 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, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise -``` - -Types: [Agent](../subsystems/core.md) · [LlmCallConfig](../subsystems/core.md) · [Scoped](../subsystems/scope.md) - -Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts) - -### `agent/request-error` — waterfall - -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 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, payload: { agent: Agent; turn: number; step: number; provider: string; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined; signal: AbortSignal }, next: () => Promise): Promise -``` - -Types: [Agent](../subsystems/core.md) · [LlmFailure](../subsystems/llm-streaming.md) · [RequestErrorAction](../subsystems/core.md) · [ResolvedRetryPolicy](../subsystems/llm-streaming.md) · [Scoped](../subsystems/scope.md) - -Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts) - -### `agent/session-start` — emit - -The session lifecycle began, once before the first turn. Use `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. - -```ts cordis-catalog -/** - * The session lifecycle began, once before the first turn. Use - * `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 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, payload: { agent: Agent; source: SessionStartSource }): void -``` - -Types: [Agent](../subsystems/core.md) · [Scoped](../subsystems/scope.md) · [SessionStartSource](../subsystems/core.md) - -Source: [`packages/core/agent/src/types.ts:216`](../../packages/core/agent/src/types.ts) - -### `agent/status` — emit - -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`). 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, payload: { agent: Agent; status: AgentStatus }): void -``` - -Types: [Agent](../subsystems/core.md) · [AgentStatus](../subsystems/core.md) · [Scoped](../subsystems/scope.md) - -Source: [`packages/core/agent/src/types.ts:177`](../../packages/core/agent/src/types.ts) - -### `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 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 -/** - * 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. - * @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, payload: { agent: Agent; turn: number; signal: AbortSignal }): Promise | void -``` - -Types: [Agent](../subsystems/core.md) · [Scoped](../subsystems/scope.md) - -Source: [`packages/core/agent/src/types.ts:277`](../../packages/core/agent/src/types.ts) - -## `agent-loop/*` - -### `agent-loop/config-start-failed` — emit - -A declarative agent entry failed before it could publish a live agent. 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. - -```ts cordis-catalog -/** - * A declarative agent entry failed before it could publish a live agent. - * 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 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'(payload: { sessionId: SessionId; error: unknown }): void -``` - -Types: [SessionId](../subsystems/core.md) - -Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/src/index.ts) - -## `approval/*` - -### `approval/request` — waterfall - -Ask composed answerers for one decision. Return an outcome to claim the request or call `next()`; failure yields the fail-closed default. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -```ts cordis-catalog -/** - * Ask composed answerers for one decision. Return an outcome to claim the - * request or call `next()`; failure yields the fail-closed default. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @param req - the pending decision (agent, tool identity, reason, signal). - * @mode waterfall - */ -'approval/request'(this: Scoped, req: ApprovalRequest, next: () => Promise): Promise -``` - -Types: [ApprovalOutcome](../subsystems/approval.md) · [ApprovalRequest](../subsystems/approval.md) · [ApprovalService](../subsystems/approval.md) · [Scoped](../subsystems/scope.md) - -Source: [`packages/interaction/user-approval/src/index.ts:30`](../../packages/interaction/user-approval/src/index.ts) - -## `commands/*` - -### `commands/change` — emit - -A command was registered or unregistered. This is an unfiltered registry notification because a global or scoped change may affect any UI view. Observer failures are contained and cannot veto the registry mutation. - -```ts cordis-catalog -/** - * A command was registered or unregistered. This is an unfiltered registry - * notification because a global or scoped change may affect any UI view. - * Observer failures are contained and cannot veto the registry mutation. - * @mode emit - */ -'commands/change'(): void -``` - -Source: [`packages/interaction/commands/src/index.ts:172`](../../packages/interaction/commands/src/index.ts) - -## `credentials/*` - -### `credentials/updated` — emit - -Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Listener failures are contained and logged — a sync throw and an async rejection alike — without changing the committed operation's outcome, except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions. - -```ts cordis-catalog -/** - * Committed change to a provider-managed credential source: a `set`, an - * `unset`, or an external edit observed in storage. Ambient - * process-environment changes are not observable and never emit. Listener - * failures are contained and logged — a sync throw and an async rejection - * alike — without changing the committed operation's outcome, except - * `INVARIANT`-coded failures, which rethrow after every listener ran; - * that rethrow reaches the emitter only from synchronous listeners, so - * invariant checks on this event must not be async functions. - * @param ref - the reference whose stored value changed. - * @mode emit - */ -'credentials/updated'(ref: CredentialRef): void -``` - -Types: [CredentialRef](../subsystems/credentials.md) - -Source: [`packages/credentials/credentials/src/index.ts:67`](../../packages/credentials/credentials/src/index.ts) - -## `domain/*` - -### `domain/changed` — emit - -A domain record or the global singleton changed, emitted once per write strictly after the backend acknowledged durability. Events of one domain arrive in its write-chain order. - -```ts cordis-catalog -/** - * A domain record or the global singleton changed, emitted once per write - * strictly after the backend acknowledged durability. Events of one - * domain arrive in its write-chain order. - * @param change - domain, table (`''` for global), key (`''` for global), - * operation discriminant, and on `put` the new snapshot. - * @mode emit - */ -'domain/changed'(change: DomainChanged): void -``` - -Types: [DomainChanged](../subsystems/storage.md) - -Source: [`packages/storage/storage-domain/src/events.ts:46`](../../packages/storage/storage-domain/src/events.ts) - -## `fs/*` - -### `fs/edit-intent` — waterfall - -Single-slot decision for the next FileSystem.editText. Calling `next()` yields an unconditional edit; the first returned guard wins. - -```ts cordis-catalog -/** - * Single-slot decision for the next {@link FileSystem.editText}. Calling - * `next()` yields an unconditional edit; the first returned guard wins. - * @param target - the resolved target about to be edited. - * @param actor - the opaque tool-execution context the decider keys off. - * @mode waterfall - */ -'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined> -``` - -Types: [FsTarget](../subsystems/filesystem.md) · [FsVersion](../subsystems/filesystem.md) - -Source: [`packages/fs/fs/src/index.ts:64`](../../packages/fs/fs/src/index.ts) - -### `fs/observed` — emit - -Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited. - -```ts cordis-catalog -/** - * Record a successful observation. Listeners must be synchronous recorders: - * throws fail the tool call and returned promises are not awaited. - * @param target - the target that was read/written/edited. - * @param version - the version the actor now holds as its observation. - * @param actor - the observing tool-execution context; undefined records nothing useful. - * @mode emit - */ -'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void -``` - -Types: [FsTarget](../subsystems/filesystem.md) · [FsVersion](../subsystems/filesystem.md) - -Source: [`packages/fs/fs/src/index.ts:73`](../../packages/fs/fs/src/index.ts) - -### `fs/write-intent` — waterfall - -Single-slot decision for the next FileSystem.writeText. Calling `next()` yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers. - -```ts cordis-catalog -/** - * Single-slot decision for the next {@link FileSystem.writeText}. Calling - * `next()` yields the bare provider's unconditional write; the first listener - * that returns an intent owns the decision rather than composing with peers. - * @param target - the resolved target about to be written. - * @param actor - the opaque tool-execution context the decider keys off. - * @mode waterfall - */ -'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise): Promise -``` - -Types: [FsTarget](../subsystems/filesystem.md) · [FsWriteIntent](../subsystems/filesystem.md) - -Source: [`packages/fs/fs/src/index.ts:56`](../../packages/fs/fs/src/index.ts) - -## `goal/*` - -### `goal/changed` — emit - -Goal mutation accepted by one live agent. The matching `goal/change` session event has already committed. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -```ts cordis-catalog -/** - * Goal mutation accepted by one live agent. The matching `goal/change` - * session event has already committed. Listener failures are contained. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @param payload.agent - agent whose session owns the goal. - * @param payload.change - fresh current projection or clear tombstone. - * @mode emit - */ -'goal/changed'(this: import('@deepseek-ai/dsh-scope').Scoped, payload: { agent: Agent; change: GoalChanged }): void -``` - -Types: [Agent](../subsystems/core.md) · [GoalChanged](../subsystems/goal.md) · [Scoped](../subsystems/scope.md) - -Source: [`packages/goal/goal/src/domain.ts:114`](../../packages/goal/goal/src/domain.ts) - -## `llm/*` - -### `llm/adapters-updated` — emit - -The provider topology changed: an adapter registered or unregistered routes, or the configurable-provider directory gained or lost entries. This is a payload-free registry notification fired at each commit point (including registration disposal); consumers re-read `listProviders()`, `listModels()`, or `listConfigurableProviders()` for the new state. Observer failures are contained and cannot veto the registry mutation. - -```ts cordis-catalog -/** - * The provider topology changed: an adapter registered or unregistered - * routes, or the configurable-provider directory gained or lost entries. - * This is a payload-free registry notification fired at each commit point - * (including registration disposal); consumers re-read `listProviders()`, - * `listModels()`, or `listConfigurableProviders()` for the new state. - * Observer failures are contained and cannot veto the registry mutation. - * @mode emit - */ -'llm/adapters-updated'(): void -``` - -Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts) - -### `llm/stream` — waterfall - -Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit. - -```ts cordis-catalog -/** - * Waterfall around every streaming model call (retry, replay, routing). - * Bound to the {@link LlmService}; call `next()` to reach the resolved - * adapter's stream, or yield your own chunks to short-circuit. - * @param options - the full request. A LOOP-built request carries the - * process-local {@link markAgentLoopRequest} identity and arrives deep-frozen - * (mutation throws): its content is a pure function of the session log (the - * reconstructability Agent Note), so listeners read it, never rewrite it. - * Hand-built calls do not carry that marker; their messages already obey - * the immutable creation contract. - * @mode waterfall - */ -'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable): AsyncIterable -``` - -Types: [GenerateOptions](../subsystems/core.md) · [LlmService](../subsystems/llm-streaming.md) · [StreamChunk](../subsystems/llm-streaming.md) - -Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts) - -## `session/*` - -### `session/created` — emit - -Creation announcement during session publication. A synchronous throw vetoes and rolls back with a paired disposal; detach requested during dispatch is deferred. A returned-promise rejection is logged but cannot retroactively veto this synchronous boundary. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only sessions entered through that agent's context. - -```ts cordis-catalog -/** - * Creation announcement during session publication. A synchronous throw vetoes and rolls - * back with a paired disposal; detach requested during dispatch is deferred. - * A returned-promise rejection is logged but cannot retroactively veto this - * synchronous boundary. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners - * receive only sessions entered through that agent's context. - * @param session - the session just entered and announced. - * @dshScopeScan unsupported - * @mode emit - */ -'session/created'(this: Scoped, session: Session): void -``` - -Types: [Scoped](../subsystems/scope.md) · [Session](../subsystems/session.md) - -Source: [`packages/core/session/src/index.ts:74`](../../packages/core/session/src/index.ts) - -### `session/disposed` — emit - -Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin. Listener failures are logged and contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. - -```ts cordis-catalog -/** - * Emitted once when an announced session leaves the store, including - * publication rollback, but never for an entry whose creation announcement - * did not begin. Listener failures are logged and contained. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. - * @param session - the session that is no longer live in the store. - * @dshScopeScan unsupported - * @mode emit - */ -'session/disposed'(this: Scoped, session: Session): void -``` - -Types: [Scoped](../subsystems/scope.md) · [Session](../subsystems/session.md) - -Source: [`packages/core/session/src/index.ts:84`](../../packages/core/session/src/index.ts) - -### `session/event` — emit - -Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context. - -```ts cordis-catalog -/** - * Post-commit, fire-and-forget append feed. The listener snapshot resolves - * before the log push, but callbacks run after it; observer failures are - * logged and contained without making the committed append fail. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners - * receive only events from sessions entered through that agent's context. - * @param session - the session whose log grew. - * @param event - the appended event, exactly as recorded. - * @dshScopeScan unsupported - * @mode emit - */ -'session/event'(this: Scoped, session: Session, event: SessionEvent): void -``` - -Types: [Scoped](../subsystems/scope.md) · [Session](../subsystems/session.md) · [SessionEvent](../subsystems/core.md) - -Source: [`packages/core/session/src/index.ts:96`](../../packages/core/session/src/index.ts) - -### `session/flush` — parallel - -Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. - -```ts cordis-catalog -/** - * Awaited parallel durability checkpoint: every listener runs and the - * caller awaits all of them, with no waterfall veto. Scope-filtered dispatch - * (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. - * @param session - the session whose buffered events must reach durable storage. - * @dshScopeScan unsupported - * @mode parallel - */ -'session/flush'(this: Scoped, session: Session): Promise | void -``` - -Types: [Scoped](../subsystems/scope.md) · [Session](../subsystems/session.md) - -Source: [`packages/core/session/src/index.ts:105`](../../packages/core/session/src/index.ts) - -## `settings/*` - -### `settings/document-updated` — emit - -One registered namespace's RAW user section changed, whether or not the resolved value did. `settings/updated` is the consumer-facing event and stays deep-equal-gated; this one exists for configuration surfaces, which must learn that a field went from inherited to overridden (same resolved value, different meaning) and that their held revision is stale. Listener containment matches `settings/updated`. - -```ts cordis-catalog -/** - * One registered namespace's RAW user section changed, whether or not the - * resolved value did. `settings/updated` is the consumer-facing event and - * stays deep-equal-gated; this one exists for configuration surfaces, - * which must learn that a field went from inherited to overridden (same - * resolved value, different meaning) and that their held revision is - * stale. Listener containment matches `settings/updated`. - * @param ns - the namespace whose stored section changed. - * @param revision - the namespace's new revision. - * @mode emit - */ -'settings/document-updated'(ns: SettingsNamespace, revision: number): void -``` - -Types: [SettingsNamespace](../subsystems/settings.md) - -Source: [`packages/settings/settings/src/index.ts:170`](../../packages/settings/settings/src/index.ts) - -### `settings/updated` — emit - -Committed change to one registered namespace's resolved value. Emitted after the provider persisted (for `update`) or published (`provider`) the change; never emitted when the resolved value is deep-equal. Listener failures are contained and logged — a sync throw and an async rejection alike — except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions. - -```ts cordis-catalog -/** - * Committed change to one registered namespace's resolved value. Emitted - * after the provider persisted (for `update`) or published (`provider`) - * the change; never emitted when the resolved value is deep-equal. - * Listener failures are contained and logged — a sync throw and an async - * rejection alike — except `INVARIANT`-coded failures, which rethrow - * after every listener ran; that rethrow reaches the emitter only from - * synchronous listeners, so invariant checks on this event must not be - * async functions. - * @param ns - the namespace whose resolved value changed. - * @param next - the new resolved value. - * @param prev - the previous resolved value. - * @param source - whether the change entered through `update()` or the provider. - * @mode emit - */ -'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void -``` - -Types: [SettingsNamespace](../subsystems/settings.md) · [SettingsUpdateSource](../subsystems/settings.md) - -Source: [`packages/settings/settings/src/index.ts:157`](../../packages/settings/settings/src/index.ts) - -## `skills/*` - -### `skills/change` — emit - -A skill provider, runtime contribution, or provider-backed catalog may have changed. This is an unfiltered invalidation notification; consumers refetch the catalog for their own lookup options. Listener failures are contained and cannot veto the registry mutation. - -```ts cordis-catalog -/** - * A skill provider, runtime contribution, or provider-backed catalog may - * have changed. This is an unfiltered invalidation notification; consumers - * refetch the catalog for their own lookup options. Listener failures are - * contained and cannot veto the registry mutation. - * @mode emit - */ -'skills/change'(): void -``` - -Source: [`packages/skill/skill/src/index.ts:283`](../../packages/skill/skill/src/index.ts) - -## `subagent/*` - -### `subagent/end` — emit - -A published child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience. - -```ts cordis-catalog -/** - * A published child settled. Scope-filtered dispatch uses the same delegating - * parent carrier as `subagent/start`, so the lifecycle pair reaches the - * same scoped audience. - * @param info - the run identity and terminal outcome. - * @dshScopeScan unsupported - * @mode emit - */ -'subagent/end'(this: Scoped, info: SubagentRunEndInfo): void -``` - -Types: [Scoped](../subsystems/scope.md) · [SubagentService](../subsystems/subagent.md) - -Source: [`packages/subagent/subagent/src/index.ts:162`](../../packages/subagent/subagent/src/index.ts) - -### `subagent/provider-added` — emit - -A provider became resolvable in the registry. - -```ts cordis-catalog -/** - * A provider became resolvable in the registry. - * @param provider - the registered provider. - * @mode emit - */ -'subagent/provider-added'(provider: SubagentProvider): void -``` - -Types: [SubagentProvider](../subsystems/subagent.md) - -Source: [`packages/subagent/subagent/src/index.ts:136`](../../packages/subagent/subagent/src/index.ts) - -### `subagent/provider-removed` — emit - -A provider left the registry. Accepted runs remain holder-owned. - -```ts cordis-catalog -/** - * A provider left the registry. Accepted runs remain holder-owned. - * @param name - the provider name that no longer resolves. - * @mode emit - */ -'subagent/provider-removed'(name: string): void -``` - -Source: [`packages/subagent/subagent/src/index.ts:142`](../../packages/subagent/subagent/src/index.ts) - -### `subagent/start` — emit - -A provider established a published child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`. - -```ts cordis-catalog -/** - * A provider established a published child. For in-process providers, - * `ctx.agents.get(info.id)` resolves during this notification. - * Scope-filtered dispatch keys the carrier by the delegating parent, so a - * parent-scoped listener observes only its own delegations. Paired with - * `subagent/end`. - * @param info - the provider and published child identity. - * @dshScopeScan unsupported - * @mode emit - */ -'subagent/start'(this: Scoped, info: SubagentRunInfo): void -``` - -Types: [Scoped](../subsystems/scope.md) · [SubagentService](../subsystems/subagent.md) - -Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/subagent/src/index.ts) - -## `system-prompt/*` - -### `system-prompt/assemble` — waterfall - -Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. - -```ts cordis-catalog -/** - * Expert waterfall over the assembled sections, contexts, tools, and variables. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners - * receive only that scope's assemblies. The returned value is authoritative. - * A supplied signal controls only this explicit assembly request and must not - * be retained to control later turns. - * @param assembly - the mutable assembly built from registered providers. - * @param context - the caller's per-assembly context. - * @mode waterfall - */ -'system-prompt/assemble'(this: Scoped, assembly: PromptAssembly, context: AssembleContext, next: () => Promise): Promise -``` - -Types: [AssembleContext](../subsystems/system-prompt.md) · [Scoped](../subsystems/scope.md) · [SystemPrompt](../subsystems/system-prompt.md) - -Source: [`packages/core/system-prompt/src/index.ts:29`](../../packages/core/system-prompt/src/index.ts) - -### `system-prompt/change` — emit - -Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope. - -```ts cordis-catalog -/** - * Emitted when any prompt provider changes. This registry notification is - * unfiltered because a global change affects every scope. - * @mode emit - */ -'system-prompt/change'(): void -``` - -Source: [`packages/core/system-prompt/src/index.ts:35`](../../packages/core/system-prompt/src/index.ts) - -## `telemetry/*` - -### `telemetry/record` — waterfall - -Transform one outbound record before it reaches the backend. This waterfall is the seam's redaction extension point. It ships NO rules of its own: the innermost `next()` passes the record through unchanged, and with no listener mounted records reach the backend as captured, so exported data is exactly as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath. Dispatched synchronously on the capture hot path inside the coordinator's containment: a throwing listener withholds that one record (fail-closed) and never reaches the agent loop. Live capture dispatches at append time; on-demand capture dispatches while reading the canonical log. Redaction applies to the exported copy only; the canonical session log is never rewritten. - -```ts cordis-catalog -/** - * Transform one outbound record before it reaches the backend. This - * waterfall is the seam's redaction extension point. It ships NO rules - * of its own: the - * innermost `next()` passes the record through unchanged, and with no - * listener mounted records reach the backend as captured, so exported - * data is exactly as clean as the rules a deployment mounts. Listeners - * stack by transforming `next()`'s return value; returning without - * `next()` replaces everything beneath. Dispatched synchronously on the - * capture hot path inside the coordinator's containment: a throwing - * listener withholds that one record (fail-closed) and never reaches the - * agent loop. Live capture dispatches at append time; on-demand capture - * dispatches while reading the canonical log. Redaction applies to the - * exported copy only; the canonical session log is never rewritten. - * @param record - the candidate record, already the coordinator's own deep - * copy; listeners return a (possibly new) record and must not mutate it. - * @mode waterfall - */ -'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord -``` - -Types: [TelemetryRecord](../subsystems/telemetry.md) - -Source: [`packages/session/session-telemetry/src/index.ts:43`](../../packages/session/session-telemetry/src/index.ts) - -## `tools/*` - -### `tools/change` — emit - -A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's. - -```ts cordis-catalog -/** - * A tool was registered or unregistered, or a scoped restriction changed - * (the available tool set changed — possibly for one scope only). An - * UNFILTERED registry-subject notification, deliberately not scope-filtered - * dispatch: a global change concerns every agent's next assembly, so a - * scoped listener subscribing here sees every change, not just its own - * scope's. - * @mode emit - */ -'tools/change'(): void -``` - -Source: [`packages/core/tools/src/index.ts:191`](../../packages/core/tools/src/index.ts) - -### `tools/code-dispatch-log` — waterfall - -Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy's preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the unshaped content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. - -```ts cordis-catalog -/** - * Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before - * the bridge appends its `tool/code-dispatch` event. `next()` keeps the - * content unchanged; a listener may return replacement blocks (e.g. the - * spill policy's preview + locator for an oversized text result). Only the - * logged copy is affected — the program already received the complete - * value, and the model sees neither. A throwing listener is contained: - * the bridge falls back to logging the unshaped content. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. - * @param dispatch - the parent execution, sub-call identity, and the settled content to log. - * @mode waterfall - */ -'tools/code-dispatch-log'(this: Scoped, dispatch: CodeDispatchLog, next: () => Promise): Promise -``` - -Types: [CodeDispatchLog](../subsystems/tools.md) · [ContentBlock](../subsystems/core.md) · [Scoped](../subsystems/scope.md) · [ToolRegistry](../subsystems/tools.md) - -Source: [`packages/core/tools/src/index.ts:173`](../../packages/core/tools/src/index.ts) - -### `tools/execute` — waterfall - -Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a normalized result; wrappers may change only `exec.signal`, while call identity remains immutable. The registry re-fuses the original caller signal before the body, so replacement cannot detach caller cancellation; wrappers must still restore their signal and reach quiescence. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - -```ts cordis-catalog -/** - * Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns - * a normalized result; wrappers may change only `exec.signal`, while call - * identity remains immutable. The registry re-fuses the original caller - * signal before the body, so replacement cannot detach caller cancellation; - * wrappers must still restore their signal and reach quiescence. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - * @param exec - the allowed call about to dispatch (name, parsed arguments, caller agent, signal). - * @mode waterfall - */ -'tools/execute'(this: Scoped, exec: ToolDispatchExecution, next: () => Promise): Promise -``` - -Types: [Scoped](../subsystems/scope.md) · [ToolDispatchExecution](../subsystems/tools.md) · [ToolExecutionResult](../subsystems/tools.md) · [ToolRegistry](../subsystems/tools.md) - -Source: [`packages/core/tools/src/index.ts:148`](../../packages/core/tools/src/index.ts) - -### `tools/post-execute` — waterfall - -Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Async listeners must observe `exec.signal`; after they settle, caller cancellation replaces only a successful accepted outcome with the code selected by whether the tool body was invoked. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - -```ts cordis-catalog -/** - * Accept, replace, enrich, or block a normalized dispatch result. `next()` - * accepts it unchanged; thrown tools still reach this seam as errors. Async - * listeners must observe `exec.signal`; after they settle, caller - * cancellation replaces only a successful accepted outcome with the code - * selected by whether the tool body was invoked. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - * @param exec - the call that just ran (name, parsed arguments, caller agent). - * @param result - the dispatch outcome a listener may accept, replace, or block. - * @mode waterfall - */ -'tools/post-execute'(this: Scoped, exec: ToolExecution, result: Readonly, next: () => Promise): Promise -``` - -Types: [PostToolDecision](../subsystems/tools.md) · [Scoped](../subsystems/scope.md) · [ToolExecution](../subsystems/tools.md) · [ToolExecutionResult](../subsystems/tools.md) · [ToolRegistry](../subsystems/tools.md) - -Source: [`packages/core/tools/src/index.ts:160`](../../packages/core/tools/src/index.ts) - -### `tools/pre-execute` — waterfall - -Allow, deny, or ask before dispatch. `next()` delegates to allow; missing approval support turns `ask` into denial. Async gates must observe `exec.signal`; the registry rechecks cancellation after they settle but never abandons their promise. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - -```ts cordis-catalog -/** - * Allow, deny, or ask before dispatch. `next()` delegates to allow; missing - * approval support turns `ask` into denial. Async gates must observe - * `exec.signal`; the registry rechecks cancellation after they settle but - * never abandons their promise. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - * @param exec - the pending call (name, parsed arguments, caller agent). - * @mode waterfall - */ -'tools/pre-execute'(this: Scoped, exec: ToolExecution, next: () => Promise): Promise -``` - -Types: [PreToolDecision](../subsystems/tools.md) · [Scoped](../subsystems/scope.md) · [ToolExecution](../subsystems/tools.md) · [ToolRegistry](../subsystems/tools.md) - -Source: [`packages/core/tools/src/index.ts:137`](../../packages/core/tools/src/index.ts) - -### `tools/result` — emit - -Observe the frozen, lossless-JSON final outcome. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. - -```ts cordis-catalog -/** - * Observe the frozen, lossless-JSON final outcome. Listener failures are contained. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. - * @param exec - the execution object that traversed the pipeline. - * @param result - a deep-frozen snapshot of the final returned result. - * @mode emit - */ -'tools/result'(this: Scoped, exec: Readonly, result: Readonly): undefined -``` - -Types: [Scoped](../subsystems/scope.md) · [ToolExecution](../subsystems/tools.md) · [ToolExecutionResult](../subsystems/tools.md) · [ToolRegistry](../subsystems/tools.md) - -Source: [`packages/core/tools/src/index.ts:181`](../../packages/core/tools/src/index.ts) - -## `workflow/*` - -### `workflow/agent-end` — emit - -One `agent()` call settled (clean result, child failure, or run cancellation). Paired with Events['workflow/agent-start'] by `agent.seq`, exactly once per started call on every stop path — on an engine termination path (a worker killed past its grace) the end is engine-synthesized with outcome `'cancelled'`. - -```ts cordis-catalog -/** - * One `agent()` call settled (clean result, child failure, or run - * cancellation). Paired with {@link Events['workflow/agent-start']} by - * `agent.seq`, exactly once per started call on every stop path — on an - * engine termination path (a worker killed past its grace) the end is - * engine-synthesized with outcome `'cancelled'`. - * @param info - the run's identity snapshot. - * @param agent - the call identity plus its outcome. - * @mode emit - */ -'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void -``` - -Types: [WorkflowRunInfo](../subsystems/workflow.md) - -Source: [`packages/workflow/workflow/src/index.ts:81`](../../packages/workflow/workflow/src/index.ts) - -### `workflow/agent-start` — emit - -One `agent()` call established a published child run. Paired with Events['workflow/agent-end'] by `agent.seq`. A call that never receives a published run from the provider emits neither event in this pair. - -```ts cordis-catalog -/** - * One `agent()` call established a published child run. Paired with - * {@link Events['workflow/agent-end']} by `agent.seq`. A call that never - * receives a published run from the provider emits neither - * event in this pair. - * @param info - the run's identity snapshot. - * @param agent - the call's sequence number, label, phase, and child id. - * @mode emit - */ -'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void -``` - -Types: [WorkflowRunInfo](../subsystems/workflow.md) - -Source: [`packages/workflow/workflow/src/index.ts:70`](../../packages/workflow/workflow/src/index.ts) - -### `workflow/end` — emit - -A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves. Paired with Events['workflow/start']. - -```ts cordis-catalog -/** - * A workflow run settled (any stop reason). Fired when - * {@link WorkflowRun.result} resolves. Paired with - * {@link Events['workflow/start']}. - * @param info - the run's identity snapshot. - * @param result - the outcome data (stop reason, error, agent count) — - * deliberately WITHOUT the result value (see {@link WorkflowResultInfo}). - * @mode emit - */ -'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void -``` - -Types: [WorkflowRunInfo](../subsystems/workflow.md) - -Source: [`packages/workflow/workflow/src/index.ts:91`](../../packages/workflow/workflow/src/index.ts) - -### `workflow/log` — emit - -The script emitted a narration line (a `log(message)` call). - -```ts cordis-catalog -/** - * The script emitted a narration line (a `log(message)` call). - * @param info - the run's identity snapshot. - * @param message - the logged message, verbatim. - * @mode emit - */ -'workflow/log'(info: WorkflowRunInfo, message: string): void -``` - -Types: [WorkflowRunInfo](../subsystems/workflow.md) - -Source: [`packages/workflow/workflow/src/index.ts:60`](../../packages/workflow/workflow/src/index.ts) - -### `workflow/phase` — emit - -The script entered a phase (a `phase(title)` call) — progress grouping for observers; no execution semantics. - -```ts cordis-catalog -/** - * The script entered a phase (a `phase(title)` call) — progress grouping - * for observers; no execution semantics. - * @param info - the run's identity snapshot. - * @param title - the phase title, verbatim. - * @mode emit - */ -'workflow/phase'(info: WorkflowRunInfo, title: string): void -``` - -Types: [WorkflowRunInfo](../subsystems/workflow.md) - -Source: [`packages/workflow/workflow/src/index.ts:53`](../../packages/workflow/workflow/src/index.ts) - -### `workflow/start` — emit - -A workflow run started — the script's meta block validated, the body about to execute. Paired with Events['workflow/end']. - -```ts cordis-catalog -/** - * A workflow run started — the script's meta block validated, the body - * about to execute. Paired with {@link Events['workflow/end']}. - * @param info - the run's identity snapshot (id + meta). - * @mode emit - */ -'workflow/start'(info: WorkflowRunInfo): void -``` - -Types: [WorkflowRunInfo](../subsystems/workflow.md) - -Source: [`packages/workflow/workflow/src/index.ts:45`](../../packages/workflow/workflow/src/index.ts) - -## Inherited events (cordis core + loader/hmr/timer) - -The framework events every plugin also sees, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of the event bus, without elevating framework internals to the harness tier's prominence. - -- `internal/plugin` — A plugin fiber was created. ([`vendor/cordis/src/events.ts:328`](../../vendor/cordis/src/events.ts)) -- `internal/status` — A fiber changed lifecycle state. ([`vendor/cordis/src/events.ts:330`](../../vendor/cordis/src/events.ts)) -- `internal/service` — Interception hook for a service binding (no core producer). ([`vendor/cordis/src/events.ts:332`](../../vendor/cordis/src/events.ts)) -- `internal/update` — Waterfall: a fiber config update is being applied. ([`vendor/cordis/src/events.ts:334`](../../vendor/cordis/src/events.ts)) -- `internal/get` — Waterfall: a service is being read from the store. ([`vendor/cordis/src/events.ts:336`](../../vendor/cordis/src/events.ts)) -- `internal/set` — Waterfall: a service is being written to the store. ([`vendor/cordis/src/events.ts:338`](../../vendor/cordis/src/events.ts)) -- `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:340`](../../vendor/cordis/src/events.ts)) -- `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:342`](../../vendor/cordis/src/events.ts)) -- `hmr/change` — A watched source file changed on disk. ([`vendor/hmr/src/index.ts:20`](../../vendor/hmr/src/index.ts)) -- `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:21`](../../vendor/hmr/src/index.ts)) -- `exit` — The process is exiting on a signal. ([`vendor/loader/src/index.ts:23`](../../vendor/loader/src/index.ts)) -- `loader/config-update` — The loader config tree changed. ([`vendor/loader/src/index.ts:24`](../../vendor/loader/src/index.ts)) -- `loader/entry-init` — A config entry is being initialized. ([`vendor/loader/src/index.ts:25`](../../vendor/loader/src/index.ts)) -- `loader/partial-dispose` — An entry is being partially disposed on reload. ([`vendor/loader/src/index.ts:26`](../../vendor/loader/src/index.ts)) -- `loader/patch-context` — A context is being patched during a reload. ([`vendor/loader/src/index.ts:27`](../../vendor/loader/src/index.ts)) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md deleted file mode 100644 index 88f1762cff..0000000000 --- a/docs/cordis-catalog/services.md +++ /dev/null @@ -1,2917 +0,0 @@ - - -# Cordis Services Catalog - -Every `ctx.` service a plugin can call: the exact public interface with original method JSDoc, plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [subsystems/](../subsystems/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against. - -This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them. - -The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md). - -## `ctx.agentLoop` — `AgentLoop` - -Concrete agent factory and driver service. - -```ts cordis-catalog -/** - * Create an agent and session under one caller-supplied identity, owned by - * the accessing fiber. Constructor-driven config calls mint a fresh combined - * id before entering this boundary. - * @param id - shared agent/session identity. - * @param options - concrete loop options. - * @param meta - optional fresh-session workspace metadata. - * @returns the published running agent. - */ -create(id: SessionId, options: AgentOptions = {}, meta: Pick = {}): Agent - -/** - * Create an owned agent on a caller-supplied session id. - * @param ownerCtx - caller context that structurally owns the lifecycle. - * @param options - identities, session seed/metadata, loop options, setup, and cancellation. - * @returns the published handle. - */ -async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise - -/** - * Resume an owned agent from the configured persistence service. - * @param ownerCtx - caller context that owns load, setup, and the live lifecycle. - * @param options - persisted identity, loop options, setup, and cancellation. - * @returns the published handle. - */ -async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise -``` - -Types: [Agent](../subsystems/core.md) · [AgentOptions](../subsystems/core.md) · [SessionHeader](../subsystems/persistence.md) · [SessionId](../subsystems/core.md) - -Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts) - -## `ctx.agents` — `AgentRegistry` - -Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory. - -Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain. - -```ts cordis-catalog -/** - * Read the Agent that initiated the inherited asynchronous driver chain. - * Use this optional form for logging, tracing, metrics, or host attribution - * that also supports agentless calls. When a parent creates a child, setup - * reports the causal parent while `agentCtx.agent` identifies the child. - * @returns the inherited Agent, or `undefined` outside an initiator boundary - * and inside an explicit clearing boundary. - * @throws when this service instance has been disposed. - */ -currentInitiator(): Agent | undefined - -/** - * Read the initiating Agent and fail when no initiator boundary is active. - * Use this for private helpers contractually below a driver, or for a - * deployment-owned outbound request whose contract forbids agentless calls. - * Generic or direct-call seams use optional lookup or explicit request fields. - * @returns the inherited Agent. - * @throws when no initiator is active or this service instance has been disposed. - */ -requireInitiator(): Agent - -/** - * Run an operation with one exact Agent as its process-local initiator. The - * exact synchronous value or Promise returned by the operation is preserved. - * Custom drivers and test harnesses wrap their complete returned foreground - * lifetime. - * A queue or wire receiver may establish this boundary only after validating - * explicit identity and resolving the exact live Agent; this method does neither. - * Detached work remains owned by the subsystem that starts it. - * @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization. - * @param operation - synchronous or asynchronous operation to invoke. - * @returns the exact value returned by `operation`. - * @throws when the initiator scope is closing/disposed, or when `operation` throws. - */ -withInitiator(agent: Agent, operation: () => T): T - -/** - * Run an operation inside a boundary that hides any inherited initiating - * Agent. The exact synchronous value or Promise is preserved. - * Use this while creating lazy shared timers, queue pumps, pool maintenance, - * watchers, or exporters so they do not inherit the first Agent that happens - * to initialize them. It clears only initiator attribution, not explicit - * fields, and does not own or drain detached resources. - * @param operation - synchronous or asynchronous operation to invoke without an initiator. - * @returns the exact value returned by `operation`. - * @throws when the initiator scope is closing/disposed, or when `operation` throws. - */ -withoutInitiator(operation: () => T): T - -/** - * Register the agent-creation factory (the loop calls this on construction, - * effect-scoped). A traced Cordis service is canonicalized to its concrete - * target; each create/resume call is then traced through that caller's - * context so ownership follows the caller without stacking proxy layers. - * Throws if a factory is already registered. Returns the disposer; on - * dispose the factory slot is cleared. - * @param factory - the loop-owned factory {@link create}/{@link resume} delegate to. - * @returns the disposer that clears the factory slot. The exact - * Cordis effect disposer (single-shot): composite (generator) effects may - * yield it directly — exact identity nests the teardown in order. - */ -setFactory(factory: AgentFactory): () => void - -/** - * Create and publish a new agent through the registered factory. - * Distinct from {@link register} (which records an already-constructed - * agent): this constructs the agent and its session. Rejects if no factory is - * registered or creation/setup fails. The resolved {@link AgentHandle} lets - * the owner tear down exactly this agent. - * @param options - shared identity, session seed/metadata, and agent options. - * @returns the handle after setup, rollback-covered publication, and loop start complete. - */ -async create(options: CreateAgentOptions): Promise - -/** - * Load a persisted session and resume an agent on it through the registered - * factory. Rejects if no factory is registered; the factory rejects if - * session persistence is not configured or persistence/setup fails. - * @param options - persisted identity, configuration, and optional setup. - * @returns the handle after setup, rollback-covered publication, and loop start complete. - */ -async resume(options: ResumeAgentOptions): Promise - -/** - * Register a live agent. Throws if an agent with the same id is already - * registered. Emits `agent/created` on registration and `agent/disposed` - * when the calling fiber is disposed — both with the agent's scope carrier - * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the - * emits are scope-filtered regardless of which context invoked `register` - * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always - * requires passing the carrier). Returns the disposer. - * @param agent - the already-constructed agent to record in the store. - * @returns the EXACT Cordis effect disposer (single-shot; a repeat call - * returns undefined without awaiting an in-flight teardown). Exact - * identity is load-bearing: a composite (generator) effect that owns a - * teardown ORDER — the agent factory's lifecycle chain — must yield THIS - * function so Cordis nests the unregistration at that yield position; - * yielding a wrapper would leave it disposing as a concurrent sibling on - * owner unload, unregistering the agent (and emitting `agent/disposed`) - * while its final turn is still draining. - */ -register(agent: Agent): () => void - -/** - * Insert an already-constructed agent without announcing it. This is the - * advanced ordered-lifecycle primitive used by the async agent factory: it - * first completes setup while the agent is unpublished, then assigns the - * returned detach closure into its pre-installed composite teardown before - * calling {@link announce}. Ordinary callers use {@link register}. - * @param agent - the prepared, unpublished agent. - * @param owner - live agent whose scoped context created this agent, or - * undefined for a top-level runtime root. This is runtime ownership, not - * the resumed session's durable parent lineage. - * @returns an idempotent closure that removes this exact entry and emits - * `agent/disposed` with listener failures contained. When called from a - * synchronous `agent/created` listener, removal and disposal wait until - * that creation dispatch unwinds. - */ -enter(agent: Agent, owner: Agent | undefined): () => void - -/** - * Announce an agent previously inserted with {@link enter}. - * @param agent - the live inserted agent to announce. - * @throws if `agent` is not the exact live registry entry for its id, or its - * creation announcement already began (including a reentrant call from a - * creation listener). - */ -announce(agent: Agent): void - -/** - * Look up a live agent. - * @param id - the shared agent/session id to look up. - * @returns the agent, or undefined when no live agent has that id. - */ -get(id: SessionId): Agent | undefined - -/** - * Test whether a live agent was created through one exact parent agent's - * scoped context. Runtime ownership is independent of durable session - * lineage and remains unambiguous when unrelated providers reuse an id. - * @param id - the candidate child agent's shared agent/session id. - * @param owner - the expected runtime creator agent. - * @returns true only while the exact child entry is live under that owner. - */ -isOwnedBy(id: SessionId, owner: Agent): boolean - -/** - * All live agents, in registration order. - * @returns a fresh array; mutating it does not affect the registry. - */ -list(): Agent[] - -/** - * All live top-level agents in registration order. A top-level agent was - * created without an owning agent context; durable session lineage does not - * affect this runtime relation, so a resumed fork may still be a root. - * @returns a fresh array; mutating it does not affect the registry. - */ -roots(): Agent[] -``` - -Types: [Agent](../subsystems/core.md) · [SessionId](../subsystems/core.md) - -Source: [`packages/core/agent/src/index.ts:253`](../../packages/core/agent/src/index.ts) - -## `ctx.approval` — `ApprovalService` - -Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session. It exposes deterministic policy changes to the model through the runtime-context snapshot and switch notices. - -```ts cordis-catalog -/** - * Switch one live agent's policy and queue the transition for its next model - * step. Session initialization uses {@link setApprovalPolicy} directly - * because there is no previously visible policy to change. - * @param agent - the live agent whose policy is changing. - * @param policy - the new effective policy. - */ -setPolicy(agent: Agent, policy: ApprovalPolicy): void - -/** - * Ask the composed answerers to decide one readonly same-process request. - * The service borrows the request, agent, session, and live signal directly. - * The request requires an open turn because the audit pair must be enclosed - * by the durable log's commit/replay boundary; an idle ask rejects before - * appending anything. The answerer phase always produces an outcome: an - * aborted signal yields `'cancelled'`, a missing or throwing answerer yields - * `'unavailable'` (fail closed), and a rogue non-vocabulary return value is - * normalized to `'unavailable'`. A failure that prevents either audit append - * from committing still rejects because returning an unlogged decision would - * violate the pair. Session contains post-commit observer failures, so an - * authoritative append cannot reject the request or suppress its matching - * audit event. - * @param req - the pending decision (agent, tool identity, reason, signal). - * @returns the closed outcome; `'allowed-once'` is the only grant. - * @throws when no turn is open or either audit event fails before the session - * append commit point. - */ -async request(req: ApprovalRequest): Promise - -/** - * Read the session override without applying the configured default. - * @param session - session whose log supplies the override. - * @returns the last logged policy, or `undefined` without one. - */ -overrideOf(session: Session): ApprovalPolicy | undefined -``` - -Types: [Agent](../subsystems/core.md) · [ApprovalOutcome](../subsystems/approval.md) · [ApprovalPolicy](../subsystems/approval.md) · [ApprovalRequest](../subsystems/approval.md) · [Session](../subsystems/session.md) - -Source: [`packages/interaction/user-approval/src/index.ts:193`](../../packages/interaction/user-approval/src/index.ts) - -## `ctx.bash` — `BashExecutor` (abstract seam) - -Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). - -Implementations must honor these semantics: - -- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult. -- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr. -- BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files. -- A still-running background process is stopped and awaited when its owning composition tears down. With the subprocess seam that boundary is `ctx.subprocess` disposal, so a background process survives an executor-only reload. - -```ts cordis-catalog -/** - * Apply implementation-owned defaults and caps to a request before execution. - * @param request - the caller's request; omitted fields get this - * implementation's defaults, capped fields are clamped. - * @returns the fully-specified spec to hand to {@link run}/{@link start}. - */ -abstract resolve(request: BashExecRequest): BashExecSpec - -/** - * Run a command in the foreground; resolves when it finishes. - * @param spec - a resolved spec from {@link resolve}, never a raw request. - * @returns the outcome; nonzero exits, timeout kills, and abort kills - * resolve with a descriptive result rather than reject. - */ -abstract run(spec: BashExecSpec): Promise - -/** - * Start a background process and return its handle immediately. - * @param spec - a resolved spec from {@link resolve}, never a raw request. - * @returns the live process handle (reads, kill, quiescence promise). - */ -abstract start(spec: BashExecSpec): BashProcess -``` - -Types: [BashExecRequest](../subsystems/bash.md) · [BashExecSpec](../subsystems/bash.md) · [BashProcess](../subsystems/bash.md) · [BashRunResult](../subsystems/bash.md) - -Source: [`packages/bash/bash/src/index.ts:53`](../../packages/bash/bash/src/index.ts) - -## `ctx.bashEnv` — `BashEnvRegistry` - -Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model shell call: ambient `DSH_*` values are discarded by the executor, then the registry's current snapshot is injected. Built-in shell facts remain owned by the registry itself while plugins can register additional, enumerable facts with effect-scoped disposal. - -```ts cordis-catalog -/** - * Register one environment contributor. Names and keys are unique; built-in - * keys are reserved. Registration is disposed with the calling plugin fiber. - * @param contributor - declared key ownership and per-execution resolver. - * @returns the disposer that unregisters the contribution. - */ -register(contributor: BashEnvContributor): () => void - -/** - * Build the trusted `DSH_*` snapshot for one shell tool execution. - * @param execution - the current tool execution. - * @returns an immutable environment overlay containing built-ins and current contributions. - */ -collect(execution: ToolExecution): DshEnvironment - -/** - * Enumerate plugin-contributed variables without executing their resolvers. - * @returns declarations sorted by environment variable name. - */ -list(): BashEnvVariableInfo[] -``` - -Types: [DshEnvironment](../subsystems/subprocess.md) · [ToolExecution](../subsystems/tools.md) - -Source: [`packages/bash/bash-env/src/index.ts:89`](../../packages/bash/bash-env/src/index.ts) - -## `ctx.clientModuleHost` — `ClientModuleHostService` - -The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it). - -```ts cordis-catalog -/** - * Current composed entry graph (stable object between changes). - * @returns the graph served as `window.__DSH_BOOT__`. - */ -graph(): WebBootGraph - -/** - * Absolute path of an entry's client bundle. - * @param id - entry id (package name). - * @returns the path, or undefined for an unknown id. - */ -clientPath(id: string): string | undefined - -/** - * Re-hash one bundle (the HMR watch's registration hook — the only entry - * point through which bundle content changes reach the graph). - * @param id - entry id (package name). - * @returns the new rev, or undefined for an unknown id. - */ -rebuilt(id: string): string | undefined - -/** - * Subscribe to bundle rebuilds; fires only when the re-hash changed the rev. - * @param listener - receives the entry id and its new bundle rev. - * @returns the unsubscriber. - */ -onRebuilt(listener: (id: string, rev: string) => void): () => void - -/** - * Fires after any flush that recomposed the graph (row added/removed, or a - * rebuilt rev change). Pull model: listeners re-read {@link graph}. - * @param listener - notified with no payload. - * @returns the unsubscriber. - */ -onGraphChanged(listener: () => void): () => void -``` - -Types: [WebBootGraph](../subsystems/client-modules.md) - -Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts) - -## `ctx.codeRuntime` — `CodeRuntime` (abstract seam) - -Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and substrate failures resolve in CodeRunResult; only seam misuse rejects. Implementations bridge structured-cloneable bindings, materialize each declared namespace rejection class, treat programs as hostile peers, isolate runs from one another, and terminate and await in-flight runs during disposal. - -```ts cordis-catalog -/** - * Execute one program against the request's bindings and capture what it - * emitted. See the class doc for the resolution contract (error is a result - * field; rejection means seam misuse only). - * @param request - the program, its bindings, and the abort signal; the - * request carries everything the runtime acts on, with no hidden defaults. - * @returns the run's outcome: completion value (when transferable), the - * ordered log capture, and the failure (if any). - */ -abstract run(request: CodeRunRequest): Promise -``` - -Types: [CodeRunRequest](../subsystems/code-runtime.md) · [CodeRunResult](../subsystems/code-runtime.md) - -Source: [`packages/code-runtime/code-runtime/src/index.ts:104`](../../packages/code-runtime/code-runtime/src/index.ts) - -## `ctx.commands` — `CommandService` - -Human-command registry. Plain-context definitions are global; definitions registered through a command-injected child of an agent context shadow globals for that agent. - -```ts cordis-catalog -/** - * Register a global or calling-agent-scoped command. - * @param definition - discovery metadata and direct UI handler. - * @returns the exact effect disposer that unregisters this definition. - */ -register(definition: CommandDefinition): () => void - -/** - * List the effective immutable command descriptors for one agent. - * @param agent - exact receiving agent and scoped-layer key. - * @returns name-sorted descriptors after scoped shadowing. - */ -list(agent: Agent): readonly CommandDescriptor[] - -/** - * Resolve one effective command definition. - * @param agent - exact receiving agent and scoped-layer key. - * @param name - command name without a slash. - * @returns the scoped shadow or global definition. - */ -find(agent: Agent, name: string): CommandDefinition | undefined - -/** - * Parse and execute a known command without sending it to the model. - * - * A resolved command's lifecycle is logged: `command/run` is appended - * before the handler is invoked and `command/done` after settlement (a - * thrown or aborted handler settles as `kind: 'error'`). Both are direct - * log-only appends — no turn wraps them, and persistence drains them at - * ordinary checkpoints. Admission misses (syntax or unknown name) log - * nothing — they never entered a handler. A `command/run` append failure - * fails the execution loud; a `command/done` append failure on the - * handler-failure path is contained so the handler's own error stays the - * reported failure. - * - * @param agent - exact receiving agent. - * @param line - complete slash-command line. - * @param signal - cancellation signal owned by the UI request. - * @returns the settled execution (result + lifecycle pairing id), or - * `undefined` when syntax or name does not resolve. - */ -async execute( agent: Agent, line: string, signal: AbortSignal, ): Promise -``` - -Types: [Agent](../subsystems/core.md) · [CommandDefinition](../subsystems/commands.md) · [CommandDescriptor](../subsystems/commands.md) - -Source: [`packages/interaction/commands/src/index.ts:305`](../../packages/interaction/commands/src/index.ts) - -## `ctx.compact` — `CompactService` (abstract seam) - -Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses COMPACT_CHECKPOINT_SOURCE so consumers recognize it independently of the backend. Load one implementation per context as `ctx.compact`. - -```ts cordis-catalog -/** - * Consider automatic compaction for one explicit trigger. Pressure policy - * uses the latest durable routed request, while context-overflow policy may - * force a useful balanced reduction even below the normal threshold. Return - * `null` when no safe range can be compacted. A single oversized retained - * unit or request envelope cannot be repaired through surface compaction. - * - * @param agent - agent context owning the session surface and routing options. - * @param trigger - normal pressure or provider-confirmed context overflow. - * @param signal - cancellation signal; model-backed implementations must forward it. - * @returns the compaction result, or `null` if no compaction was needed. - */ -abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger, signal: AbortSignal, ): Promise - -/** - * Explicitly compact useful history even below automatic pressure thresholds. - * Implementations synchronously start an idle task before any asynchronous - * work, select a useful range without writing on a no-op, then - * append a standalone `compact/start` before summarization. That durable - * marker is the compaction lock until one `compact/end` attempt. Later waking - * prompts remain accepted in FIFO order and start only after the optional - * durability checkpoint and idle-task settlement. Context injected while the - * summary runs may sit between the marker pair; only the selected span must - * remain stable. - * - * @param agent - idle agent whose durable history should be compacted. - * @param signal - cancellation scoped to this compaction request. - * @returns the compaction result, or `null` when no safe useful range exists. - * @throws {@link ManualCompactionError} for expected busy, agent-cancellation, - * changed-span, summarization/shrink, commit-stage, or persistence failures; - * an aborted request preserves its exact abort reason. Failed attempts remain - * visible in the log. - */ -abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): Promise - -/** - * Forcibly compact a range of surface nodes into a single summary node. - * `start` and `end` name an inclusive span by surface position, not numeric seq - * order; replacements can make visible seqs non-monotonic. Both edges must be - * balanced so assistant tool calls remain paired with their results. A model- - * backed implementation forwards cancellation and rejects active, missing, - * reversed, or unbalanced ranges. The target session is `agent.session`. - * Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}. - * Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter} - * for the edge checks. - * - * @param start - first surface seq, inclusive. - * @param end - last surface seq, inclusive. - * @param agent - context whose session is mutated and whose routing options guide summarization. - * @param signal - optional cancellation; model-backed implementations must forward it. - * @throws when compaction is active or the range is missing, reversed, or unbalanced. - * @returns the appended event seqs, summary, replaced range, and token accounting. - */ -abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise -``` - -Types: [CompactionResult](../subsystems/compaction.md) · [CompactionTrigger](../subsystems/compaction.md) - -Source: [`packages/compact/compact/src/index.ts:93`](../../packages/compact/compact/src/index.ts) - -## `ctx.credentials` — `Credentials` (abstract seam) - -Abstract credential service. Providers implement the four operations over their source layers; one seam-wide rule binds them all: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret. - -```ts cordis-catalog -/** - * Resolve one reference to its current value. Resolution is per call: - * consumers re-resolve at each operation and must not cache across - * operations — that per-operation read is what makes a changed credential - * reach the next operation without a restart. - * @param ref - the reference to resolve. - * @returns the value and its source, or `undefined` while unconfigured. - */ -abstract resolve(ref: CredentialRef): Promise - -/** - * Describe one reference for configuration surfaces without exposing the - * value. - * @param ref - the reference to describe. - * @returns configured state, supplying source, and writability. - */ -abstract describe(ref: CredentialRef): Promise - -/** - * Durably store one value in the provider-managed writable source. Rejects - * while a read-only source shadows the reference — the write would appear - * to succeed while resolution keeps returning the shadowing value — and - * rejects an empty value (use {@link unset}). - * @param ref - the reference to store. - * @param value - the non-empty secret value. - */ -abstract set(ref: CredentialRef, value: string): Promise - -/** - * Remove one reference from the provider-managed writable source; removing - * an absent reference is a no-op. Rejects while a read-only source shadows - * the reference, like {@link set}. - * @param ref - the reference to remove. - */ -abstract unset(ref: CredentialRef): Promise -``` - -Types: [CredentialInfo](../subsystems/credentials.md) · [CredentialRef](../subsystems/credentials.md) · [ResolvedCredential](../subsystems/credentials.md) - -Source: [`packages/credentials/credentials/src/index.ts:77`](../../packages/credentials/credentials/src/index.ts) - -## `ctx.directoryPicker` — `DirectoryPicker` (abstract seam) - -Abstract directory-picking service. Subclass, implement `capability()`, and load the subclass as a plugin — it registers as `ctx.directoryPicker` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior). The capability object must be stable for the service lifetime: consumers may capture it across calls. - -```ts cordis-catalog -/** - * The backend's interaction capability. - * @returns the discriminated capability consumers switch on. - */ -abstract capability(): DirectoryPickerCapability -``` - -Source: [`packages/host/directory-picker/src/index.ts:131`](../../packages/host/directory-picker/src/index.ts) - -## `ctx.e2b` — `E2BSandboxService` - -Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal. Creation begins at plugin construction; adapters await getSandbox before their first operation. - -```ts cordis-catalog -/** - * Return the shared live SDK handle. - * @returns the created sandbox after the configured cwd exists. - * @throws when E2B rejects creation or the service is disposing. - */ -async getSandbox(): Promise -``` - -Source: [`packages/e2b/e2b/src/index.ts:74`](../../packages/e2b/e2b/src/index.ts) - -## `ctx.fs` — `FileSystem` (abstract seam) - -Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract. - -```ts cordis-catalog -/** - * Resolve a model/plugin-supplied path into a stable {@link FsTarget}. May perform I/O (a - * remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence - * async even though the local backend only normalizes + realpaths. - * - * @param path - the path to resolve; relative paths resolve against `opts.cwd`. - * @param opts - optional cwd override and cancellation signal. - * @returns the stable target; the same file yields the same `targetKey`. - */ -abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise - -/** - * Return the canonical absolute path a subprocess in this filesystem's - * execution world can open. The path is deliberately separate from - * {@link FsTarget.targetKey}: consumers may pass this value to another OS - * capability, but must continue treating the target key as opaque. - * @param target - the resolved target whose process path is required. - * @returns an absolute path in the backend's execution world. - */ -abstract processPath(target: FsTarget): string - -/** - * Return the canonical `file:` URI for a target in this filesystem's - * execution world. Backends own URI encoding because the host platform may - * differ from the execution platform. - * @param target - the resolved target to encode. - * @returns the target's canonical file URI. - */ -abstract fileUrl(target: FsTarget): string - -/** - * Test canonical containment without exposing or parsing backend target - * keys. Both targets must come from this provider. - * @param parent - canonical directory target. - * @param child - canonical candidate target. - * @returns true when `child` is `parent` or a descendant of it. - */ -abstract contains(parent: FsTarget, child: FsTarget): boolean - -/** - * Return target metadata, or `undefined` when the target does not exist. - * @param target - the resolved target to stat. - * @param signal - aborts the metadata round-trip. - * @returns metadata only, never content; undefined for an absent target. - */ -abstract stat(target: FsTarget, signal?: AbortSignal): Promise - -/** - * Return path metadata without following the final path component when it is a - * symbolic link. This is intentionally path-shaped, not target-shaped: - * {@link resolve} follows symlinks to produce the stable identity used by - * normal reads/writes, while `lstat` lets a consumer reject the path itself - * before that follow happens. - * - * `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is - * absent. - * @param path - the path to inspect; relative paths resolve against `opts.cwd`. - * @param opts - `cwd` overrides the backend's default base for relative paths. - * @param signal - aborts the metadata round-trip. - * @returns metadata only, never content; undefined for an absent path. - */ -abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise - -/** - * Read the whole regular text file as a single decoded string. - * @param target - the resolved target to read. - * @param signal - aborts the read. - * @returns the full decoded UTF-8 content. - */ -abstract readText(target: FsTarget, signal?: AbortSignal): Promise - -/** - * Stream the whole regular text file as decoded text chunks (same text - * semantics as {@link readText}, for large files). The backend owns - * cross-chunk UTF-8 decoding and binary rejection so the policy layer never - * touches raw bytes. - * @param target - the resolved target to read. - * @param signal - aborts the stream, including between chunks. - * @returns the chunk iterable, decoded and validated like {@link readText}. - */ -abstract streamText(target: FsTarget, signal?: AbortSignal): Promise> - -/** - * List direct children of a directory in stable name order. Returns resolved - * child targets plus cheap metadata only; never reads file contents. - * @param target - the resolved directory target. - * @param signal - aborts the listing. - * @returns one entry per direct child, in stable name order. - */ -abstract listDir(target: FsTarget, signal?: AbortSignal): Promise - -/** - * Atomically create or replace UTF-8 text. `expected` guards intent and - * staleness; omission allows unconditional overwrite. - * @param target - the resolved target to write. - * @param content - the full new file content. - * @param expected - the write intent guarding the write; omit for unconditional. - * @param signal - aborts before the atomic rename takes effect. - * @param sandboxPolicy - the per-call mode and workspace root this write - * runs under; a sandboxing backend fences the write by it, the bare backend - * ignores it. Omit to leave the backend its own default. - * @returns the outcome, including the version the write produced. - */ -abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise - -/** - * Atomically edit literal text. When supplied, the version guard is checked - * before matching so stale content reports `FS_STALE_VERSION`; omission edits - * the current content without a freshness precondition. - * @param target - the resolved target to edit. - * @param edit - the literal search/replace request. - * @param expected - the version guard; omit for an unconditional edit. - * @param signal - aborts before the atomic rename takes effect. - * @param sandboxPolicy - the per-call mode and workspace root this edit runs - * under; a sandboxing backend fences the edit by it, the bare backend - * ignores it. Omit to leave the backend its own default. - * @returns the outcome, including the version the edit produced. - */ -abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise -``` - -Types: [FsDirEntry](../subsystems/filesystem.md) · [FsEditOutcome](../subsystems/filesystem.md) · [FsEditRequest](../subsystems/filesystem.md) · [FsInfo](../subsystems/filesystem.md) · [FsPathInfo](../subsystems/filesystem.md) · [FsTarget](../subsystems/filesystem.md) · [FsVersion](../subsystems/filesystem.md) · [FsWriteIntent](../subsystems/filesystem.md) · [FsWriteOutcome](../subsystems/filesystem.md) · [SandboxExecutionPolicy](../subsystems/sandbox.md) - -Source: [`packages/fs/fs/src/index.ts:83`](../../packages/fs/fs/src/index.ts) - -## `ctx.goals` — `GoalService` - -Goal service (`ctx.goals`) backed exclusively by the owning session log. - -```ts cordis-catalog -/** - * Read the current goal for one exact live agent. - * @param agent - owning live agent. - * @returns a fresh view or `undefined` when no goal is current. - * @throws {@link GoalError} when the agent is not the registry's live instance. - */ -get(agent: Agent): GoalView | undefined - -/** - * Remove process-local continuation authority without changing durable goal - * phase or revision. Lifecycle owners use this before unloading a driver; - * a later human-authorized {@link resume} records the new activation edge. - * @param agent - owning live agent. - * @returns a fresh disarmed view, or `undefined` when no goal is current. - */ -disarm(agent: Agent): GoalView | undefined - -/** - * Create and arm a goal. A completed goal may be replaced; every other - * current phase must be cleared or resumed instead. - * @param agent - owning live agent. - * @param request - objective and optional round cap. - * @returns the created live view. - */ -create(agent: Agent, request: CreateGoalRequest): GoalView - -/** - * Edit objective and/or round cap without changing phase. - * @param agent - owning live agent. - * @param ref - expected current revision. - * @param request - at least one replacement field. - * @returns the edited view. - */ -@Remote('edit') edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView - -/** - * Pause an active goal and disarm automatic continuation. - * @param agent - owning live agent. - * @param ref - expected current revision. - * @returns the paused view. - */ -@Remote('pause') pause(agent: Agent, ref: GoalRef): GoalView - -/** - * Resume and arm a stopped goal, or rearm an active goal after a - * session-start edge, while its round budget still has capacity. - * @param agent - owning live agent. - * @param ref - expected current revision. - * @returns the active view. - */ -@Remote('resume') resume(agent: Agent, ref: GoalRef): GoalView - -/** - * Mark a current non-complete goal complete and disarm it. - * @param agent - owning live agent. - * @param ref - expected current revision. - * @returns the completed view. - */ -@Remote('complete') complete(agent: Agent, ref: GoalRef): GoalView - -/** - * Mark an active goal blocked and disarm it. - * @param agent - owning live agent. - * @param ref - expected current revision. - * @param reason - policy-owned stable code and human-readable explanation. - * @returns the blocked view with its durable reason. - */ -block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView - -/** - * Clear the current goal while retaining a durable tombstone and history. - * @param agent - owning live agent. - * @param ref - expected current revision. - * @returns the tombstone ref whose revision is one past the cleared snapshot. - */ -@Remote('clear') clear(agent: Agent, ref: GoalRef): GoalRef - -/** - * Create one Goal through the remote boundary. - * @param agent - exact live Agent resolved from the wire identity. - * @param request - objective and optional round cap. - * @returns the created Goal identity. - */ -@Remote('create') remoteExportCreate(agent: Agent, request: CreateGoalRequest): CreateGoalResult -``` - -Types: [Agent](../subsystems/core.md) · [CreateGoalRequest](../subsystems/goal.md) · [CreateGoalResult](../subsystems/goal.md) · [EditGoalRequest](../subsystems/goal.md) · [GoalBlockReason](../subsystems/goal.md) · [GoalRef](../subsystems/goal.md) · [GoalView](../subsystems/goal.md) - -Source: [`packages/goal/goal/src/index.ts:183`](../../packages/goal/goal/src/index.ts) - -## `ctx.httpServer` — `HttpServerService` - -The web-shape HTTP carrier service. Activation listens immediately (route registration order carries no request-facing semantics: named routes are composed to be disjoint, and the fallback seat answers anything not yet claimed during the boot window — 404 until its owner registers). A listen failure throws out of init — a FAILED fiber the boot's fail-loud sweep reports. - -```ts cordis-catalog -/** - * Register a named route. Duplicate (kind, path) throws — route patterns are - * a composition-level contract, so a collision is a misconfiguration. - * @param route - kind, path, and the owning handler. - * @returns the disposer removing the route. - */ -register(route: WebRoute): () => void - -/** - * Register an exact-path HTTP upgrade route. Duplicate paths throw because - * one socket can have only one protocol owner. - * @param route - pathname and handler owning negotiation plus socket use. - * @returns the disposer removing the route. - */ -registerUpgrade(route: WebUpgradeRoute): () => void - -/** - * Claim the fallback seat: the handler answering every request no named - * route matches (the SPA dist server in the shipped Web composition). One - * owner only — a second registration throws, because two fallbacks cannot - * compose. - * @param handler - owns the full response lifecycle of unmatched requests. - * @returns the disposer releasing the seat. - */ -registerFallback(handler: WebRoute['handler']): () => void - -/** - * Register an index.html transform, applied by the fallback owner to every - * index response ({@link applyIndexTaps}) in registration order. - * @param transform - pure html-to-html function. - * @returns the disposer removing the transform. - */ -tapIndex(transform: (html: string) => string): () => void - -/** - * Run an index.html body through the registered taps in registration order - * — called by the fallback owner on every index response it renders. - * @param html - the raw index.html body. - * @returns the transformed body. - */ -applyIndexTaps(html: string): string -``` - -Types: [WebRoute](../subsystems/http-server.md) - -Source: [`packages/host/webserver/src/index.ts:60`](../../packages/host/webserver/src/index.ts) - -## `ctx.invariants` — `InvariantService` - -Package-owned invariant registry with global and regex-based selection. - -```ts cordis-catalog -/** - * Register one package's invariant installer. The package name is reserved - * even when filtering disables its checks. Enabled installers run in a child - * fiber; failure disposes that fiber and releases the reservation. - * @param packageName - full npm package name that owns the contribution. - * @param installer - listener or startup-check installer for the child context. - * @returns an effect-scoped disposer for the registration. - */ -register(packageName: string, installer: InvariantInstaller): () => void -``` - -Types: [InvariantInstaller](../subsystems/invariants.md) - -Source: [`packages/support/invariants/src/index.ts:94`](../../packages/support/invariants/src/index.ts) - -## `ctx.llm` — `LlmService` - -The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall. - -```ts cordis-catalog -/** - * Register an adapter for the given provider routes. Throws `LlmError` with code - * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing). - * Disposed with the fiber. - * @param providers - every provider route this adapter should serve. - * @param adapter - the adapter that streams calls for those providers. - * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}. - */ -registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle - -/** - * Describe provider routes with a registered adapter. - * @returns detached provider metadata in registration order. - */ -listProviders(): LlmProviderInfo[] - -/** - * Declare provider routes an adapter plugin can activate through - * configuration. Registration is all-or-nothing: an empty list, invalid - * 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 a handle that withdraws all of them, and can atomically replace them. - */ -registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle - -/** - * List every declared configurable provider, registered or dormant. - * @returns detached directory entries in declaration order. - */ -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, ): () => 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 - -/** - * Resolve the retry policy captured when one provider route was registered. - * @param provider - registered provider route to inspect. - * @returns the provider-owned policy, with normal defaults already resolved. - */ -providerRetryPolicy(provider: string): ResolvedRetryPolicy - -/** - * Discover models advertised by one registered provider. Catalog membership - * is advisory and never changes routing or request validation. - * @param provider - registered provider route to inspect. - * @returns detached model metadata in adapter-preferred order. - */ -async listModels(provider: string): Promise - -/** - * Resolve and validate all metadata from the adapter that owns one exact - * route. The result is detached from adapter-owned objects; catalog - * membership remains advisory and does not control request routing. - * @param provider - registered provider route to inspect. - * @param model - exact model id passed to the adapter. - * @param signal - optional cancellation for adapter-owned asynchronous lookup. - * @returns exact model identity plus available context and reasoning metadata. - */ -async resolveModelInfo( provider: string, model: string, signal?: AbortSignal, ): Promise - -/** - * Validate a conversation call config against its exact model capability and - * materialize adapter-configured defaults. Unsupported explicit efforts - * reject before provider I/O; no clamping or aliasing is performed. This - * standalone query does not bind a later dispatch; use {@link prepareCall} - * when logging and streaming must share one adapter registration. - * @param config - provider/model route and optional request controls. - * @param signal - optional cancellation for adapter-owned capability lookup. - * @returns a detached config only when a default must be materialized. - */ -async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise - -/** - * Resolve one call under its current adapter registration. The returned - * one-shot handle keeps that registration across header logging and dispatch, - * so HMR cannot combine one adapter's capability result with another adapter. - * @param config - provider/model route and optional request controls. - * @param signal - optional cancellation for adapter-owned capability lookup. - * @returns a prepared config and its registration-bound stream entry point. - */ -async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise - -/** - * 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 -``` - -Types: [AdapterRegistrationHandle](../subsystems/core.md) · [DirectoryRegistrationHandle](../subsystems/core.md) · [GenerateOptions](../subsystems/core.md) · [LlmAdapter](../subsystems/llm-streaming.md) · [LlmCallConfig](../subsystems/core.md) · [LlmConfigurableProvider](../subsystems/core.md) · [LlmDiscoveredModel](../subsystems/core.md) · [LlmModelDiscoveryRequest](../subsystems/core.md) · [LlmModelInfo](../subsystems/core.md) · [LlmProviderInfo](../subsystems/core.md) · [LlmResolvedModelInfo](../subsystems/core.md) · [PreparedLlmCall](../subsystems/llm-streaming.md) · [ResolvedRetryPolicy](../subsystems/llm-streaming.md) · [StreamChunk](../subsystems/llm-streaming.md) - -Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts) - -## `ctx.permission` — `PermissionService` - -Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error. - -```ts cordis-catalog -/** - * Resolve the preset matching the effective knob values. A still-matching - * last selection wins shared-bundle ties; otherwise the first table match - * wins, or {@link CUSTOM_PRESET} when no entry matches. - * @param events - the session's events in log order. - * @returns the effective preset name, or `custom` when nothing matches. - */ -current(events: readonly SessionEvent[]): string - -/** - * Build the whole select value for one folded knob state: every table - * option in declaration order, `custom` appended exactly while derived. - * @param state - the folded knob overrides. - * @returns the `permissions` projection payload. - */ -selectFor(state: KnobState): PermissionSelect - -/** - * Resolve a preset's knob bundle. - * @param name - the preset name to resolve. - * @returns the configured bundle. - * @throws when `name` is not in the table. - */ -resolve(name: string): PresetSpec - -/** - * Build the client option for a table entry or {@link CUSTOM_PRESET}. A - * missing label falls back to the table key. - * @param name - a table key, or `custom`. - * @returns the option a client renders. - * @throws when `name` is neither a table key nor `custom`. - */ -optionOf(name: string): PresetOption - -/** - * Record a changed preset, then update each changed knob through its own - * setter. Selecting the effective preset again appends nothing. - * @param session - the session the switch belongs to. - * @param name - the preset to switch to; unknown names throw. - */ -set(session: Session, name: string): void -``` - -Types: [PresetOption](../subsystems/permission.md) · [PresetSpec](../subsystems/permission.md) · [Session](../subsystems/session.md) · [SessionEvent](../subsystems/core.md) - -Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/src/index.ts) - -## `ctx.planMode` — `PlanModeService` - -`ctx.planMode`: owns logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool. UIs observe committed flips through `session/event`; there is no live mirror. - -```ts cordis-catalog -/** - * Read the logged plan state and any selected state awaiting a boundary. - * - * @param agent The agent to read. - * @returns Current logged state plus a pending selection, when present. - */ -get(agent: Agent): { active: boolean; pending?: boolean } - -/** - * Select whether plan mode should be active. Between turns the change - * commits immediately — no request boundary would arrive until the next - * prompt, so a queued intent would hang (the open-turn fold is the idle - * signal: agent status stays `running` through post-turn checkpointing, - * where a boundary equally never comes). During an open turn the - * selection is held as pending intent for the next in-turn request - * boundary. Repeated selection of the current or already-pending state is - * a no-op. - * - * @param agent The agent to switch. - * @param active Whether plan mode should be active. - * @returns what happened: `committed` (logged now), `queued` (awaiting the - * next boundary), `cancelled` (an opposite pending selection was cleared; - * the logged state already matches), or `noop` (already in that state). - */ -set(agent: Agent, active: boolean): 'committed' | 'queued' | 'cancelled' | 'noop' -``` - -Types: [Agent](../subsystems/core.md) - -Source: [`packages/plan/plan-mode/src/index.ts:183`](../../packages/plan/plan-mode/src/index.ts) - -## `ctx.pty` — `PtyService` - -In-process registry for replaceable PTY backends and exact-Agent sessions. - -```ts cordis-catalog -/** - * Register one backend type for this effect scope. - * @param backend - provider with a non-empty unique type. - * @returns disposer that removes exactly this contribution. - */ -registerBackend(backend: PtyBackend): () => void - -/** - * List registered backend types in registration order. - * @returns fresh backend type names. - */ -listBackends(): string[] - -/** - * Create and publish one owner-scoped session after backend setup succeeds. - * @param owner - exact registered Agent that owns access and cleanup. - * @param request - backend type plus optional owner-local name and cwd. - * @param signal - cancellation of unpublished setup. - * @returns published identity, metadata, status, and MOTD. - */ -async spawn(owner: Agent, request: PtySpawnRequest, signal?: AbortSignal): Promise - -/** - * Test whether an exact owner has a published session or unpublished spawn. - * @param owner - exact live owner to inspect. - * @returns true across the entire spawn-to-close interval, with no publication gap. - */ -hasOwnerActivity(owner: Agent): boolean - -/** - * Start one exclusive interactive send. - * @param owner - exact session owner. - * @param id - target PTY identity. - * @param request - explicit text, submit behavior, and cancellation. - * @returns live operation handle for foreground await or task registration. - */ -startSend(owner: Agent, id: PtySessionId, request: PtySendRequest): PtySendOperation - -/** - * Read one bounded scrollback page from an owned session. - * @param owner - exact session owner. - * @param id - target PTY identity. - * @param request - optional newest-relative offset and line count. - * @returns bounded retained text and pagination metadata. - */ -read(owner: Agent, id: PtySessionId, request: PtyReadRequest = {}): PtyReadResult - -/** - * Deliver an allowed signal through an owned backend session. - * @param owner - exact session owner. - * @param id - target PTY identity. - * @param signal - allowed POSIX signal name. - * @returns delivered foreground process-group identity. - */ -signal(owner: Agent, id: PtySessionId, signal: PtySignal): Promise - -/** - * Close one owned session and remove it only after quiescent backend cleanup. - * @param owner - exact session owner. - * @param id - target PTY identity. - * @param reason - diagnostic cleanup reason. - * @returns true for a newly closed session, false when the same close is already in flight. - */ -async kill(owner: Agent, id: PtySessionId, reason: string = 'model request'): Promise - -/** - * List fresh snapshots for exactly one owner. - * @param owner - exact owner whose sessions are visible. - * @returns owner-visible snapshots in publication order. - */ -list(owner: Agent): PtySessionSnapshot[] -``` - -Types: [Agent](../subsystems/core.md) · [PtyBackend](../subsystems/pty.md) · [PtyReadRequest](../subsystems/pty.md) · [PtyReadResult](../subsystems/pty.md) · [PtySendOperation](../subsystems/pty.md) · [PtySendRequest](../subsystems/pty.md) · [PtySessionId](../subsystems/pty.md) · [PtySessionSnapshot](../subsystems/pty.md) · [PtySignal](../subsystems/pty.md) · [PtySignalResult](../subsystems/pty.md) · [PtySpawnRequest](../subsystems/pty.md) · [PtySpawnResult](../subsystems/pty.md) - -Source: [`packages/pty/pty/src/index.ts:105`](../../packages/pty/pty/src/index.ts) - -## `ctx.sandbox` — `SandboxProvider` (abstract seam) - -Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end. - -```ts cordis-catalog -/** - * Wrap `argv` so it executes confined under `policy` on this host; the - * caller spawns the returned argv in place of its own. - * @param argv - the exact argv the caller is about to spawn (program plus - * arguments), NOT a shell string — a shell-shaped consumer passes - * `['bash', '-c', command]`. - * @param policy - the file-effect policy this execution runs under, - * carried per call (see {@link SandboxPolicy}). - * @returns the argv to spawn instead, plus the enforcement completeness - * the selected backend achieves for it. - */ -abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv -``` - -Types: [ConfinedArgv](../subsystems/sandbox.md) · [SandboxPolicy](../subsystems/sandbox.md) - -Source: [`packages/sandbox/sandbox/src/index.ts:148`](../../packages/sandbox/sandbox/src/index.ts) - -## `ctx.sandboxPolicy` — `SandboxPolicyService` - -The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability. - -```ts cordis-catalog -/** - * Resolve the complete policy for one capability call. An approved explicit - * mode outranks the session's last `sandbox/mode` event, which outranks the - * deployment default. A session cwd is its workspace-write boundary; the - * configured root is the fallback for agentless calls and sessions without a - * cwd. - * @param request - optional session and approved mode override. - * @returns the fully resolved per-call mode and absolute workspace root. - */ -resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy - -/** - * Read the session override without applying the deployment default. - * @param session - session whose log supplies the override. - * @returns the last logged mode, or `undefined` without one. - */ -overrideOf(session: Session): SandboxMode | undefined -``` - -Types: [SandboxExecutionPolicy](../subsystems/sandbox.md) · [SandboxMode](../subsystems/sandbox.md) · [SandboxPolicyRequest](../subsystems/sandbox.md) · [Session](../subsystems/session.md) - -Source: [`packages/sandbox/sandbox-policy/src/index.ts:91`](../../packages/sandbox/sandbox-policy/src/index.ts) - -## `ctx.sessionPersistence` — `SessionPersistence` (abstract seam) - -Durable append-only session storage. Implementations preserve contiguous, losslessly JSON-serializable events; append resolves only after durability, and load balances a complete interrupted tail without rewriting committed events. - -```ts cordis-catalog -/** - * Resolve this backend's independent local artifact for a session without - * reading, creating, flushing, or otherwise materializing it. Backends such - * as SQLite that do not own one artifact per session return `undefined`. - * @param meta - the immutable session header whose artifact is requested. - * @returns the backend-specific absolute location, when one exists. - */ -abstract locate(meta: SessionHeader): SessionLocation | undefined - -/** - * Register a new session's metadata. A backend MAY defer the physical write - * until the first {@link append} (lazy materialization), in which case a - * created-but-never-appended session is absent from {@link list} - * — abandoned sessions leave nothing behind. - * @param meta - the immutable header (id, version, cwd, lineage) to record. - */ -abstract create(meta: SessionHeader): Promise - -/** - * Durably persist a batch of events. Honors the append-only and contiguous- - * seq contracts: the first event's `seq` MUST equal the stored next-seq - * (after `load` has durably closed any interrupted turn). Rejects non-JSON- - * serializable `event.data` with an error naming the offending event type. - * @param id - the session the batch belongs to. - * @param events - the contiguous batch to persist, in seq order. - */ -abstract append(id: SessionId, events: readonly SessionEvent[]): Promise - -/** - * Prepare the exact unpublished Session used by resume. Implementations may - * reuse object graphs retained by an earlier {@link inspect} after confirming - * their durable revision is still current; disposal releases an unpublished - * reservation. Revision retries require the durable log to remain unchanged - * for one read/check round trip; continuous external writers may delay completion. - * @param id - persisted session to prepare. - * @param signal - optional cancellation for preparation work. - * @returns one owned unpublished Session preparation. - */ -async prepare(id: SessionId, signal?: AbortSignal): Promise - -/** - * Load an immutable balanced logical view and commit any required cold - * recovery. A complete interrupted final turn is preserved and durably - * closed with missing tool errors plus any open step and turn boundaries; - * only a torn final record is discarded. Unknown versions and corruption in - * the committed prefix reject. Implementations MUST NOT crash-repair an - * identity still bound to a live Session: a balanced live log may return as a - * durable snapshot, while an open live turn rejects. Returned values may be - * shared with immutable live or prepared state and must not be mutated. - * Revision-based implementations may wait for one stable read/check round trip. - * @param id - the persisted session to reload. - * @returns the header and a log ending on a balanced `turn/end`. - */ -abstract load(id: SessionId): Promise - -/** - * Inspect an immutable logical session without committing recovery or - * publishing it. A cold complete interrupted turn receives synthetic closers - * in memory and a torn physical tail remains untouched. An already-live - * Session instead yields its current immutable snapshot, which may contain an - * open turn and its `session/end-seed` boundary. Coordinator-backed - * implementations retain the exact cold unpublished Session for bounded - * reuse by a later {@link prepare}. A stale ready source is reloaded; a source - * already committing or reserved for resume remains exclusive, and inspection - * may borrow its immutable view. Callers borrow only the immutable header and - * log. Continuous external writers may delay revision convergence. - * @param id - the persisted session to inspect. - * @param signal - optional cancellation for queued and backend read work. - * @returns the validated header and current logical event log. - */ -abstract inspect(id: SessionId, signal?: AbortSignal): Promise - -/** - * Read the stored events from `fromSeq` onward — the read-from-seq - * primitive for read models that resume from a watermark (e.g. a persisted - * projection cache folding only the tail past its checkpoint). Unlike - * {@link inspect}, it is a detached physical suffix read: no preparation - * cache, torn-tail truncation, synthetic closers, or coordinator-state - * publication. Only events from the valid contiguous stored prefix are - * returned, so a torn fragment never reaches the caller. `fromSeq` at or - * beyond the stored prefix returns an empty event list (never an error). - * Backends whose medium can seek by seq - * (SQLite) read only the suffix; sequential media (JSONL, both encodings) - * still parse the whole artifact and skip forward — the primitive bounds - * what is RETURNED and refolded, not every backend's physical read. - * @param id - the persisted session to read. - * @param fromSeq - first event seq to include; a non-negative safe integer. - * @param signal - optional cancellation for queued and backend read work. - * @returns the header and the stored events with `seq >= fromSeq`. - */ -abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }> - -/** - * Lightweight listing from metadata, without a full-log parse. - * @param signal - optional cancellation for backend listing work. - * @returns one header per materialized session. - */ -abstract list(signal?: AbortSignal): Promise - -/** - * List materialized sessions with cheap per-log change tokens. - * - * Repeated observations of an unchanged log return the same revision. A - * successful mutating {@link load} repair changes the next listed revision. - * Revisions also distinguish independently backed stores so backend-local - * counters cannot compare equal across different persistence sources. - * @param signal - optional cancellation for backend snapshot-listing work. - * @returns one header and opaque revision per materialized session without loading full logs. - */ -abstract listSnapshots(signal?: AbortSignal): Promise -``` - -Types: [SessionEvent](../subsystems/core.md) · [SessionHeader](../subsystems/persistence.md) · [SessionId](../subsystems/core.md) · [SessionInspection](../subsystems/persistence.md) · [SessionLocation](../subsystems/persistence.md) · [SessionPersistenceSnapshot](../subsystems/persistence.md) · [SessionPreparation](../subsystems/persistence.md) - -Source: [`packages/session/session-persistence/src/index.ts:72`](../../packages/session/session-persistence/src/index.ts) - -## `ctx.sessionProjectionCache` — `SessionProjectionCache` - -The persisted projection cache service. Opens the `session_projcache` domain at init, checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus two mandatory points — `turn/end` and session disposal (the live-to-cold moment) — and serves the cold-read ladder: cached row, persistence `readFrom` tail, registry `restore`, durable write-back. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write or cold read. - -```ts cordis-catalog -/** - * The zero-I/O listing read: whole values viewed straight from the stored - * rows (version-matching keys only), each cut carried with its watermark - * so a client value store can seed under its higher-seq-wins rule — as - * stale as the last durable checkpoint but never wrong, and never from an - * unrelated log (the caller's header is the identity witness). Fresher - * paths (the history tail baseline, {@link coldSnapshot}) supersede these - * values whenever a session is actually opened. - * @param meta - the listed session's header (identity witness; no log read). - * @returns the cut (`asOfSeq` = lowest served-row watermark), or - * `undefined` when no usable row exists for this lifecycle. - */ -cachedSnapshot(meta: SessionHeader): ProjectionSnapshot | undefined - -/** - * Durably checkpoint one live session NOW (both mandatory points call - * this; tests and carriers may too). The registry cut is snapshotted at - * this boundary (states are live references), then the whole record is - * replaced. NOT fail-soft — callers on the fail-soft paths contain it. - * @param session - the live session to checkpoint. - * @returns resolution after durability and event emission. - */ -async write(session: Session): Promise - -/** - * Cold-read one persisted session's projections with zero full-log load: - * cached rows + a persistence `readFrom` tail from the registry's restore - * floor, refolded by the registry and written back (fail-soft) so the next - * cold read starts closer. A cache row invalidated by a shrunk log - * (crash-repair truncation) triggers one full re-read from seq 0 — the - * ladder's slow rung, still no crash. Rejects when the session has no - * persisted log (`not found` from the persistence seam). - * @param id - the persisted session to read. - * @param signal - optional cancellation for the persistence reads. - * @returns the snapshot cut at the stored log end. - */ -async coldSnapshot(id: SessionId, signal?: AbortSignal): Promise -``` - -Types: [Session](../subsystems/session.md) · [SessionHeader](../subsystems/persistence.md) · [SessionId](../subsystems/core.md) - -Source: [`packages/session/session-projection-cache/src/index.ts:71`](../../packages/session/session-projection-cache/src/index.ts) - -## `ctx.sessionProjections` — `SessionProjectionRegistry` - -`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. Duplicate keys throw. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. - -```ts cordis-catalog -/** - * Register one domain's unit. The registration is an effect on the calling - * context's fiber: disposing the fiber (or calling the returned disposer) - * removes the key — and the unit's cached cells — from subsequent drives - * and snapshots. - * @param definition - key, boundary schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. - */ -register(definition: ProjectionDefinition): () => void - -/** - * Subscribe to the change feed. The registration is an effect on the - * calling context's fiber. - * @param listener - called once per unit whose state reference changed, per committed event. - * @returns the exact disposer that unsubscribes. - */ -onChanged(listener: ProjectionChangeListener): () => void - -/** - * One consistent cut over every registered unit for one session, read from - * the watermark cache (missing cells fold lazily over the in-memory log). - * Fully synchronous — every value and `asOfSeq` reflect the same log - * position. Each value passes its unit's schema before leaving. - * @param session - the session whose projection values are read. - * @returns the snapshot; `values` is empty when no unit is registered. - */ -snapshot(session: Session): ProjectionSnapshot - -/** - * State-level checkpoint of every registered unit for one session, read - * from the watermark cache (missing cells fold lazily over the in-memory - * log). This is the write side of the persisted projection cache: the - * returned rows are the `(key → {ver, seq, val})` part of the durable - * `(sessionId, key, ver, seq, val)` - * rows. Every `val` is a DETACHED structured clone — never the live - * cell reference: the watermark cache is this registry's authoritative - * mutable state, and a caller reaching the live reference could corrupt - * every subsequent snapshot and frame through it (plain JSON by the unit - * contract, so the clone is total). - * @param session - the session whose unit states are checkpointed. - * @returns one row per registered key; empty when no unit is registered. - */ -checkpoint(session: Session): ProjectionCheckpoint - -/** - * The stored seq a {@link restore} tail read over `checkpoint` must start - * at: one event BELOW the lowest usable watermark (a row is usable when - * its `ver` matches the live unit's `stateVersion`; an absent or mismatched row - * pulls the floor to `0` — that key must refold the full log). The - * one-below anchor is load-bearing: the tail then proves how far the - * stored log still extends, so {@link restore} can detect a log that - * shrank below a row's watermark (crash-repair truncation) instead of - * serving the stale row as current — an empty tail read from the anchor - * yields an end below every watermark and the restore rejects for a full - * re-read. - * @param checkpoint - persisted rows for one session (possibly stale or empty). - * @returns the seq to hand the persistence `readFrom`, or `undefined` - * when no unit is registered (no read needed — {@link restore} would - * serve empty values regardless). - */ -restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined - -/** - * View a checkpoint's rows without any log read: for every registered - * unit whose row's `ver` matches, serve the schema-validated - * `view` of the stored state; mismatched or absent rows leave their key - * absent (a cold or listing consumer treats it as not-yet-available and a - * fuller read path refolds it). The zero-I/O rung of the read ladder — - * values are as stale as their rows, never wrong. - * @param checkpoint - persisted rows for one session (possibly stale or empty). - * @returns whole values per key with a usable row; empty when none. - */ -viewCheckpoint(checkpoint: ProjectionCheckpoint): Partial - -/** - * Cold read: fold every registered unit over a stored log suffix, seeding - * each from its checkpoint row when usable — the one read recipe (cached - * state + forward tail replay + `view`) applied without a live `Session`. - * Call with the events returned by a persistence - * `readFrom(id, restoreFloor(checkpoint))` and that same floor as - * `baseSeq`; the floor's one-below anchor makes the supplied end honest, - * so a shrunk log is detected here. A row is usable iff its - * `ver` matches the live unit's `stateVersion`, it does not predate `baseSeq` - * (`seq >= baseSeq - 1`), and it does not claim events past the - * supplied end (`seq <= endSeq`); an unusable row is discarded - * and its key refolds from `init` — which is only sound over the full - * log, so a discarded row with `baseSeq > 0` throws (the caller re-reads - * from seq 0, e.g. after a crash-repair truncation shrank the log below - * a row's watermark). - * @param checkpoint - persisted rows for one session (possibly stale or empty). - * @param events - the stored events with `seq >= baseSeq`, in seq order. - * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty). - * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last - * supplied event's seq, `baseSeq - 1` for an empty tail) plus the - * refreshed checkpoint rows at that cut, ready for a durable write-back. - */ -restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } -``` - -Types: [Session](../subsystems/session.md) · [SessionEvent](../subsystems/core.md) - -Source: [`packages/session/session-projection/src/index.ts:156`](../../packages/session/session-projection/src/index.ts) - -## `ctx.sessionQuery` — `SessionQueryService` (abstract seam) - -Unified live-preferred session query service. - -Exact reads, filters, and traces are backend-independent concrete behavior. A backend implements full-text observation, reconciliation, ranking, cursor generations, and query execution on the same `ctx.sessionQuery` service. - -```ts cordis-catalog -/** - * Search the live-preferred logical corpus and group by session. - * @param request - query text, metadata filters, page size, and cursor. - * @param exec - optional cancellation control. - * @returns session hits ranked by their strongest matching event. - */ -abstract searchSessions( request: SessionSearchRequest, exec?: SessionSearchExecContext, ): Promise> - -/** - * Search events within one live-preferred logical session. - * @param request - target session, query text, filters, page size, and cursor. - * @param exec - optional cancellation control. - * @returns matching event hits and their target header from one indexed generation. - */ -abstract searchEvents( request: SessionEventSearchRequest, exec?: SessionSearchExecContext, ): Promise - -/** - * List the complete logical corpus using live-preferred records. - * @param signal - optional cancellation for persistence listing. - * @returns deterministic newest-first cloned session records. - */ -listSessions(signal?: AbortSignal): Promise - -/** - * Read and replay-validate one complete logical session log without making it live. - * @param sessionId - live or persisted session id to read. - * @returns cloned header and complete raw event log from one observation. - * @throws when persistence, header compatibility, or replay validation fails. - */ -async readSession(sessionId: SessionId): Promise - -/** - * Filter the complete logical corpus with provider-independent predicates. - * @param filters - ANDed session metadata and availability clauses. - * @param signal - optional cancellation for persistence listing. - * @returns matching cloned records in deterministic newest-first order. - */ -async filterSessions( filters: readonly SessionResultFilter[], signal?: AbortSignal, ): Promise - -/** - * Fold the latest log-backed title from one live-preferred logical session. - * @param sessionId - live or persisted session id to read. - * @param signal - optional cancellation for source resolution and title folding. - * @returns latest title snapshot, or `undefined` when the log has no title event. - */ -async readTitle( sessionId: SessionId, signal?: AbortSignal, ): Promise - -/** - * Fold the latest title and return its source header from one corpus observation. - * @param sessionId - live or persisted session id to read. - * @param signal - optional cancellation for source resolution and title folding. - * @returns cloned source header and optional latest title snapshot. - */ -async readTitleSnapshot( sessionId: SessionId, signal?: AbortSignal, ): Promise - -/** - * Fold titles for unique sessions from one cancellable corpus observation. - * - * Results preserve first-occurrence input order. Operational failures stay - * isolated per session, while cancellation rejects the complete operation. - * @param sessionIds - live or persisted session ids to observe. - * @param signal - optional cancellation shared by all source reads. - * @returns one fulfilled or rejected result per unique requested id. - */ -async readTitleSnapshots( sessionIds: readonly SessionId[], signal?: AbortSignal, ): Promise - -/** - * List lightweight raw-log event records for one logical session. - * @param sessionId - live-preferred session id to read. - * @returns event records in ascending seq order. - */ -async listEvents(sessionId: SessionId): Promise - -/** - * Scan first-party semantic event documents with provider-independent filters. - * @param sessionId - live-preferred session id to scan. - * @param filters - ANDed metadata and literal-text predicates. - * @returns matching semantic documents in ascending seq order. - */ -async filterEvents( sessionId: SessionId, filters: readonly SessionEventResultFilter[], ): Promise - -/** - * Read one session's complete current model surface from one corpus observation. - * @param sessionId - live-preferred session id to read. - * @returns cloned header, current surface, and raw-log capture boundary. - * @throws when source resolution fails or the session surface is invalid. - */ -async readSurface(sessionId: SessionId): Promise - -/** - * Trace known ancestry and descendants from one corpus observation. - * @param sessionId - logical session id to trace. - * @param signal - optional cancellation for persistence listing. - * @returns a complete lineage or an explicit unresolved parent boundary. - * @throws when corpus resolution fails, the target is absent, or its known ancestry cycles. - */ -async traceSession(sessionId: SessionId, signal?: AbortSignal): Promise - -/** - * Trace one event's direct positional and provenance relationships. - * @param request - target session id and event seq. - * @param signal - optional cancellation for persisted source resolution. - * @returns source header, direct links, and the target's positional replacement chain. - * @throws when source resolution fails, the target is absent, or surface/provenance validation fails. - */ -async traceEvent(request: SessionEventTraceRequest, signal?: AbortSignal): Promise - -/** - * Read one full event plus a bounded raw-log context window. - * @param request - target session/seq and context sizes. - * @param signal - optional cancellation for persisted source resolution. - * @returns cloned target and neighboring events. - */ -async readEvent(request: SessionEventReadRequest, signal?: AbortSignal): Promise -``` - -Types: [SessionEventReadRequest](../subsystems/session-query.md) · [SessionEventRecord](../subsystems/session-query.md) · [SessionEventResultFilter](../subsystems/session-query.md) · [SessionEventSearchDocument](../subsystems/session-query.md) · [SessionEventSearchPage](../subsystems/session-query.md) · [SessionEventSearchRequest](../subsystems/session-query.md) · [SessionEventTraceObservation](../subsystems/session-query.md) · [SessionEventTraceRequest](../subsystems/session-query.md) · [SessionEventWindow](../subsystems/session-query.md) · [SessionId](../subsystems/core.md) · [SessionLineageTrace](../subsystems/session-query.md) · [SessionLogSnapshot](../subsystems/session-query.md) · [SessionRecord](../subsystems/session-query.md) · [SessionResultFilter](../subsystems/session-query.md) · [SessionSearchExecContext](../subsystems/session-query.md) · [SessionSearchHit](../subsystems/session-query.md) · [SessionSearchPage](../subsystems/session-query.md) · [SessionSearchRequest](../subsystems/session-query.md) · [SessionSurfaceSnapshot](../subsystems/session-query.md) · [SessionTitleObservation](../subsystems/session-query.md) · [SessionTitleObservationResult](../subsystems/session-query.md) · [SessionTitleSnapshot](../subsystems/session-title.md) - -Source: [`packages/session-query/session-query/src/index.ts:81`](../../packages/session-query/session-query/src/index.ts) - -## `ctx.sessionReferences` — `SessionReferenceService` - -Exact-read consumer that prepares immutable cross-session message context. - -```ts cordis-catalog -/** - * List reference candidates, ranked by working-directory affinity. - * @param agent - target agent; self is excluded and its cwd drives ranking. - * @param query - optional case-insensitive session-id/cwd/title substring. - * @param limit - optional positive result cap. - * @param signal - optional cancellation boundary for host autocomplete teardown. - * @returns candidates labeled by latest title or, when absent, session id. - */ -async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise - -/** - * Snapshot all references before enqueue and return one aggregated durable context. - * @param agent - target agent; references to it are rejected. - * @param content - already host-normalized readable message content. - * @param references - structured source sessions in mention order. - * @param signal - optional cancellation boundary for host request teardown. - * @returns detached content and optional referenced-session context. - */ -async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise -``` - -Types: [Agent](../subsystems/core.md) · [ContentBlock](../subsystems/core.md) · [PreparedReferencedMessage](../subsystems/session-reference.md) · [SessionReferenceCandidate](../subsystems/session-reference.md) · [SessionReferenceInput](../subsystems/session-reference.md) - -Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts) - -## `ctx.sessions` — `SessionStore` - -In-memory session store (`ctx.sessions`). - -Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose. - -```ts cordis-catalog -/** - * Create a session owned by the calling fiber: disposing that fiber stops - * event notification and removes the session from the store. `options.seed` - * populates the session with a copy of those events (replay/fork); - * `options.meta` attaches creation metadata (validated absolute `cwd`, seed - * and parent lineage, and delegation depth) as the immutable - * {@link SessionHeader} (the store fills `version`/`id`/`createdAt`). - * - * For an agent whose session must be torn down IN ORDER with its loop (so the - * loop's final events are published before the store attachment ends), do NOT use this - * — fold the session lifecycle into the agent's own effect via - * {@link prepare} + {@link enter} + {@link announce} (see - * `dsh-agent-loop`'s creation transaction). - * - * @param id - the session id; omitted, the store mints `session-`. - * @param options - seed events and/or creation metadata for the header. - * @returns the live session, already entered and announced. - * @throws if a session with `id` already exists, metadata is not a plain - * lossless-JSON record with valid scalar fields, or `meta.cwd` is a - * non-absolute path (storage backends key directories off it). - */ -create(id?: SessionId, options?: CreateSessionOptions): Session - -/** - * Build a session WITHOUT entering it into the store — validate the id/cwd and - * construct the {@link Session} (with its immutable {@link SessionHeader}). - * Pairs with {@link enter} + {@link announce}: a caller that owns a composite - * `ctx.effect` (the agent factory) folds the session lifecycle into that ONE - * effect so a fiber unload tears the session + agent down as a single ORDERED - * chain rather than as racing sibling effects — which would remove the publication hooks - * before the driver's closing events commit, dropping them. - * - * @param id - the session id; omitted, the store mints `session-`. - * @param options - seed events and/or creation metadata for the header. With - * `seedSource: 'persistence'`, metadata and events must be fresh detached - * graphs whose ownership transfers to this call: they are validated and - * frozen in place through {@link Session.fromRestore}, so the caller must - * retain no mutable aliases. - * @returns the constructed session, NOT yet in the store. - * @throws if a session with `id` already exists, metadata is not a plain - * lossless-JSON record with valid scalar fields, or `meta.cwd` is a - * non-absolute path. - */ -prepare(id?: SessionId, options?: PrepareSessionOptions): Session - -/** - * Enter a {@link prepare}d session into the store: install the module-private - * append publication hooks and add it to the store. Returns the DETACH - * disposer (hooks + store removal). Does NOT emit `session/created` — - * the caller yields this disposer inside its effect and THEN calls - * {@link announce}, so a throwing `session/created` listener rolls the attach - * back instead of leaking it. - * - * Re-checks the id for a duplicate: `prepare` and `enter` are public - * cross-package primitives and a caller may interleave arbitrary work (or - * another create) between them, so a stale prepared session must NOT overwrite - * a live store entry of the same id — its detach disposer would later delete - * the REAL session. The {@link create} convenience and the agent factory call - * the two back-to-back so they never trip this, but the public seam cannot - * assume that. - * - * @param session - a {@link prepare}d session not yet in the store. - * @returns the detach disposer (publication hooks + store removal). When called from - * a synchronous `session/created` listener, removal and disposal wait until - * that creation dispatch unwinds. - * @throws if a session with this id is already in the store. - */ -enter(session: Session): () => void - -/** Emit `session/created` exactly once for an {@link enter}ed session (with - * the carrier {@link enter} captured). Separate from {@link enter} so the - * caller can yield the detach disposer first (rollback safety — see - * {@link enter}). - * @param session - the entered session to announce to listeners. - * @throws if the session is not live or its announcement already began, - * including a reentrant call from a creation listener. */ -announce(session: Session): void - -/** - * Dispatch the awaited `session/flush` durability checkpoint for `session`, - * with the carrier captured at {@link enter}. THE flush entry point: the - * store owns the carrier, so callers (the checkpoint policy's per-request - * barrier, goal-session's idle checkpoint, teardown drains, and consumers - * that flush themselves before reading storage) must come through here - * rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner, - * one spelling, and the scoped-dispatch invariant can pin it. - * @param session - the session whose buffered events must reach durable storage. - * @returns whether at least one durability listener participated, after every - * listener has settled successfully. - * @throws the first registered listener failure after every listener settles. - */ -async flush(session: Session): Promise - -/** - * Look up a live session. - * @param id - the session id to look up. - * @returns the session, or undefined when no live session has that id. - */ -get(id: SessionId): Session | undefined - -/** - * All live sessions, in creation order. - * @returns a fresh array; mutating it does not affect the store. - */ -list(): Session[] - -/** - * Create a live child session from a stable prefix of a live source. - * `boundary` is an inclusive source event seq; omitted means the source's - * current last event. The selected slice may end with a between-turn event - * but must not end inside an open turn. - * - * @param source - Live source session object or id. - * @param boundary - Inclusive source event seq to fork through; omitted means - * the source's current last event, and omitted on an empty source forks an - * empty child. - * @param childSessionId - Optional child session id; omitted delegates to - * `SessionStore`'s id policy. - * @returns The created live child session. - */ -fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session -``` - -Types: [CreateSessionOptions](../subsystems/persistence.md) · [PrepareSessionOptions](../subsystems/persistence.md) · [Session](../subsystems/session.md) · [SessionId](../subsystems/core.md) - -Source: [`packages/core/session/src/index.ts:807`](../../packages/core/session/src/index.ts) - -## `ctx.sessionTitle` — `SessionTitleService` - -Log-backed title fold plus asynchronous fallback generation. - -```ts cordis-catalog -/** - * Read the latest folded title from one live or replayed session. - * @param session - session whose log is the title source of truth. - * @returns latest title snapshot, or `undefined` before eligible input. - */ -get(session: Session): SessionTitleSnapshot | undefined - -/** - * Accept an explicit user title. Appends a `session/title` event with the - * `user` source, which pins the title: in-flight automatic generation is - * superseded and later user messages schedule none (an explicit - * {@link SessionTitleService.refresh} remains the deliberate unpin). - * @param session - exact live session to rename. - * @param title - raw user input; normalized before acceptance. - * @returns the accepted title snapshot. - * @throws {SessionTitleInvalidError} when the title normalizes to empty. - * @throws {Error} when the session is not live or the service is disposed. - */ -rename(session: Session, title: string): SessionTitleSnapshot - -/** - * Explicitly retry the registered provider, or materialize the built-in - * fallback when no provider is registered. - * @param session - exact live session to refresh. - * @param signal - optional caller cancellation. - * @returns latest accepted title, or `undefined` when no eligible text exists. - */ -async refresh(session: Session, signal?: AbortSignal): Promise - -/** - * Register the sole optional title provider. Disposal aborts its pending and - * active work before another provider may register. - * @param provider - provider identity, cadence, and generation function. - * @returns exact Cordis effect disposer, which settles after active calls quiesce. - */ -register(provider: SessionTitleProvider): () => Promise -``` - -Types: [Session](../subsystems/session.md) · [SessionTitleProvider](../subsystems/session-title.md) · [SessionTitleSnapshot](../subsystems/session-title.md) - -Source: [`packages/session/session-title/src/index.ts:261`](../../packages/session/session-title/src/index.ts) - -## `ctx.settings` — `Settings` (abstract seam) - -Abstract settings service. Providers implement raw-document storage (`load`/`persist`) and push external changes through Settings.publish; the base class owns namespace registration, resolution, validation, change detection, and the `settings/updated` commit event. - -```ts cordis-catalog -/** - * Prepare the provider's user-editable document for a native editor. File - * providers may materialize an absent document before returning its path; - * non-file providers return undefined. - * @returns the absolute local document path, or undefined for non-file storage. - */ -prepareDocument(): Promise - -/** - * Register a namespace schema and receive its owner scope. The registration - * is an effect on the calling plugin's fiber: disposing that fiber removes - * the namespace and its observers. An invalid stored section fails the - * registration itself — the earliest point where the schema can judge it. - * @param ns - unique namespace; duplicate registration fails loud. - * @param schema - schemastery schema resolving this namespace's value. - * @param options - composition `base` layer and effect timing. - * @returns the owner scope for reads, observation, and updates. - */ -register(ns: SettingsNamespace, schema: z, options?: SettingsRegisterOptions): SettingsScope - -/** - * Describe every registered namespace for configuration surfaces, including - * the composition `base` and raw user layers so a form can mark which fields - * the user overrode (presence in `user`) and what a reset returns to. - * @param options - redaction switch; wire surfaces must redact. - * @returns one descriptor per registered namespace, in registration order. - */ -describe(options?: SettingsDescribeOptions): SettingsDescriptor[] - -/** - * Read one registered namespace's resolved value. - * @param ns - the namespace to read. - * @returns the resolved value, or `undefined` while unregistered. - */ -get(ns: SettingsNamespace): unknown - -/** - * Merge a patch into one registered namespace's user layer, validate the - * resolved candidate, persist through the provider, then commit and emit. - * A validation failure rejects before anything is persisted. Writes to one - * namespace are serialized: concurrent updates apply in call order, each - * merging over the previous write's committed section. - * @param ns - the registered namespace to update. - * @param patch - plain-object patch over the user section. - * @param expectedRevision - the descriptor `revision` the caller read; a - * namespace that moved past it rejects with {@link SettingsConflictError}. - */ -async update(ns: SettingsNamespace, patch: object, expectedRevision?: number): Promise - -/** - * Replace one registered namespace's user section wholesale, validate, - * persist, then commit and emit. Keys absent from `section` fall back to the - * composition `base` and schema defaults — this is the removal/reset path a - * merge-only patch cannot express (`replace({})` re-inherits everything). - * @param ns - the registered namespace to replace. - * @param section - the complete next user section. - * @param expectedRevision - the descriptor `revision` the caller read; a - * namespace that moved past it rejects with {@link SettingsConflictError}. - */ -async replace(ns: SettingsNamespace, section: object, expectedRevision?: number): Promise - -/** - * Apply path-addressed edits to one registered namespace's user section, - * validate, persist, then commit and emit. The ops are applied to the - * section as it stands when the write reaches the front of the queue, so a - * caller never has to restate fields it did not touch — and, crucially, - * cannot delete fields it never saw. This is the write path for any caller - * holding a redacted view; `replace` remains the wholesale reset. - * @param ns - the registered namespace to edit. - * @param ops - ordered path edits; later ops observe earlier ones. - * @param expectedRevision - the descriptor `revision` the caller read; a - * namespace that moved past it rejects with {@link SettingsConflictError}. - */ -async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise -``` - -Types: [SettingsDescribeOptions](../subsystems/settings.md) · [SettingsDescriptor](../subsystems/settings.md) · [SettingsNamespace](../subsystems/settings.md) · [SettingsPathOp](../subsystems/settings.md) · [SettingsRegisterOptions](../subsystems/settings.md) · [SettingsScope](../subsystems/settings.md) - -Source: [`packages/settings/settings/src/index.ts:387`](../../packages/settings/settings/src/index.ts) - -## `ctx.skills` — `SkillService` - -Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted invocation-neutral summaries, and loads full skill bodies on demand. - -```ts cordis-catalog -/** - * Register a borrowed same-process provider synchronously during plugin apply. Duplicate and - * reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters - * the provider and invalidates catalog caches. - * @param create - synchronous factory receiving this registration's lifecycle and invalidation control. - * @returns the exact Cordis effect disposer that unregisters this provider; - * composite effects may yield it directly to preserve teardown ordering. - */ -registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void - -/** - * Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which - * outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and - * receives a no-op disposer so it cannot remove the winner. - * @param skill - the skill definition input; omitted invocation and provider fields receive defaults. - * @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches. - */ -register(skill: SkillRegistration): () => void - -/** - * List invocation-neutral skill summaries for a workspace. Consumers apply - * model or user invocation policy at their operational boundary. Lookup - * options and provider candidates are readonly same-process values borrowed - * throughout discovery. - * @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery. - * @returns all sorted winning summaries. - */ -async list(options: SkillLookupOptions = {}): Promise - -/** - * Observe the current invocation-neutral catalog and whether discovery completed within a stable revision. - * Incomplete observations are never cached, allowing consumers to retain last-good state and - * retry on their next request boundary. - * @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery. - * @returns sorted summaries plus discovery-completeness state. - */ -async snapshot(options: SkillLookupOptions = {}): Promise - -/** - * Load and validate the winning candidate, passing its opaque discovery locator back to the - * provider. Cancellation is rechecked after selection, including cache hits, and raced against - * loading so an uncooperative provider cannot hang the caller. - * @param name - kebab-case skill name. - * @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work. - * @returns the full skill, including body content, or `undefined`. - */ -async get(name: string, options: SkillLookupOptions = {}): Promise -``` - -Types: [SkillCatalogSnapshot](../subsystems/skills.md) · [SkillDefinition](../subsystems/skills.md) · [SkillLookupOptions](../subsystems/skills.md) · [SkillProvider](../subsystems/skills.md) · [SkillProviderControl](../subsystems/skills.md) · [SkillRegistration](../subsystems/skills.md) · [SkillSummary](../subsystems/skills.md) - -Source: [`packages/skill/skill/src/index.ts:304`](../../packages/skill/skill/src/index.ts) - -## `ctx.spillStore` — `SpillStore` (abstract seam) - -Abstract spill storage service. Subclass, implement saveText, and load the subclass as a plugin — it registers as `ctx.spillStore` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior). - -Semantics every implementation must honor: - -- saveText persists the FULL `content` verbatim and returns an opaque locator, exact byte length, and model-facing retrieval guidance. -- Storage is scoped by the request's SaveTextSpill.owner session; the backend chooses a private (not world-readable) location and a collision-free name derived from — never equal to — the caller's `suggestedName`. -- `saveText` REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable); the caller decides how to degrade (the spill policy treats a rejection as best-effort and keeps the inline result). - -```ts cordis-catalog -/** - * Persist `input.content` to a session-scoped spill artifact. - * @param input - the owner, provenance, suggested name, and full text to save. - * @returns the saved artifact's {@link SpillRef}; rejects on a storage failure. - */ -abstract saveText(input: SaveTextSpill): Promise -``` - -Types: [SaveTextSpill](../subsystems/spill.md) · [SpillRef](../subsystems/spill.md) - -Source: [`packages/spill/spill/src/index.ts:45`](../../packages/spill/spill/src/index.ts) - -## `ctx.storage` — `Storage` - -The storage hub service. Backends register under `backend`; data forms mount under their `StorageForms` key and are reached as `ctx.storage.
`. - -```ts cordis-catalog -/** - * Mount a data-form facility on the hub. Mounting is an effect: the - * returned disposer unmounts the form. - * @param form - Form key declared in {@link StorageForms}. - * @param facility - The facility instance to expose. - * @returns the disposer that unmounts the form. - */ -mount(form: K, facility: StorageForms[K]): () => void - -/** - * Resolve a mounted data form. - * @param form - Form key declared in {@link StorageForms}. - * @returns the mounted facility. - */ -form(form: K): StorageForms[K] -``` - -Types: [StorageForms](../subsystems/storage.md) - -Source: [`packages/storage/storage/src/index.ts:47`](../../packages/storage/storage/src/index.ts) - -## `ctx.storageDomain` — `DomainFacility` - -The mounted domain facility. Opens declared domains over routed backends; one facility instance owns the open-domain table and enforces single-open per domain name. - -```ts cordis-catalog -/** - * Open one declared domain. Steps, each failing the whole call: reject a - * name that is already open (`already-open`); resolve the backend route - * (`backend-not-found` passes through from the hub); require its `kv` facet - * (`facet-unsupported`); open the unit projected from the spec (backend - * `version-mismatch`/`malformed-medium` pass through); load and validate - * every stored record against the spec's zod schemas (`invalid-record` - * with the offending table and key); construct the domain. - * - * Lifecycle: the CALLER owns the returned handle and closes it via - * `Domain.close()` (typically as its own `ctx.effect` disposer) — the - * facility does not tie the domain to any consumer fiber. Domains still - * open when the facility unmounts are closed by the plugin disposer. - * @param spec - The domain declaration, typically from `defineDomain`. - * @returns the opened domain handle, typed by the spec. - */ -async open(spec: S): Promise> - -/** - * Look up an open domain by name, untyped. Diagnostic surface (the package - * invariant cross-checks change events against live domain state); typed - * consumers hold the handle returned by {@link open}. - * @param name - Domain name. - * @returns the open domain runtime, or `undefined` when not open. - */ -get(name: string): DomainImpl | undefined - -/** - * Close every domain still open on this facility. The unmount path for - * consumers that never called `Domain.close()` themselves; closing is - * idempotent, so double-closing an already-closed domain is harmless. - * @returns resolution after every unit is released. - */ -async closeAll(): Promise -``` - -Types: [Domain](../subsystems/storage.md) · [DomainSpec](../subsystems/storage.md) - -Source: [`packages/storage/storage-domain/src/index.ts:69`](../../packages/storage/storage-domain/src/index.ts) - -## `ctx.subagents` — `SubagentService` - -Named provider registry with one-shot runs, durable discovery, and continuable-child operations. - -```ts cordis-catalog -/** - * Establish one durable continuable child and deliver its initial prompt. - * Resolves when the child's inbox accepts that prompt, without waiting for the - * turn to start or for the message to reach the Session log; any earlier - * failure rejects with no ids and rolls back the child entirely. - * @param spec - provider, delegation request, and caller cancellation. - * @returns the durable child id and the accepted prompt's message id. - * @throws when continuation services are unavailable or materialization fails. - */ -async startContinuable(spec: ContinuableStartSpec): Promise - -/** - * Deliver one later message to a continuable child as its next FIFO turn. A - * resident child's Agent inbox accepts it directly (waking a `waiting` - * Activation), while an absent one is cold-resumed from its persisted - * Session. The Agent inbox is the only queue, so every accepted message has - * one observable order. - * @param parent - the exact live direct parent authorizing this delivery. - * @param childId - durable child session id. - * @param content - user-role content to deliver. - * @param options - durable provenance and caller cancellation, which stops the - * operation only before inbox acceptance. - * @returns the accepted message's inbox id. - * @throws when continuation services are unavailable, parent authority is - * rejected, or the message was not admitted. - */ -async followup( parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise - -/** - * Interrupt one live continuable child's current turn under a human parent - * address or an exact live ancestor Agent. Fire-and-return: the cancel - * signal is issued before this returns, but the target may keep running - * until it observes the signal. Unclaimed pending inbox work, the Activation, - * and published descendants are preserved; claimed work is not requeued. - * Once the interrupted driver is idle, a waking send resumes the parked FIFO - * queue. An absent target — including a one-shot or unknown id — - * is an accepted no-op, as is a manager-less composition, which cannot own a - * live Activation. - * @param targetSessionId - the durable child session id to interrupt. - * @param authority - the human parent address or exact live ancestor Agent. - * @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the - * live target. - */ -interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void - -/** - * Deliver selected content from one live continuable child to its durable - * direct parent. The child is the authority credential; callers cannot name a - * recipient. Reporting does not conclude the child's turn or Activation. - * @param child - exact live reporting child. - * @param content - selected model-facing content. - * @param options - parent scheduling and pre-acceptance cancellation. - * @returns the stable identity of the parent-accepted message. - * @throws when continuation services are unavailable, sender authorization - * fails, or the direct parent is not live. - */ -async reportFrom( child: Agent, content: ContentBlock[], options: SubagentReportOptions, ): Promise - -/** - * Compose one deployment capability into every continuable child's - * unpublished creation context on fresh creation and cold resume. Grants wait - * for the next Activation; removing the contribution revokes every resident - * installation immediately. - * @param contribution - synchronous child-scope installer. - * @returns the exact Cordis effect disposer. - */ -registerContinuableSetup(contribution: ContinuableSetupContribution): () => void - -/** - * Close continuable admission below exact live parent Agents, stop only their - * visible descendant Activations synchronously, then await admitted scoped - * materializations and release those forests child-first. The scoped cutoff - * lasts until each exact parent leaves the registry; unrelated parent trees - * remain live. - * @param parents - exact host-owned parent Agents entering teardown. - * @returns once every retained descendant Activation released its `AgentHandle`. - * @throws an aggregate error after all branches settle when any failed. - */ -async drainContinuableDescendants(parents: readonly Agent[]): Promise - -/** - * Enumerate the parent's direct session-backed subagents without loading or - * resuming an Agent and without any query seam: the listing merges the live - * session store with optional session persistence (live-preferred) and - * serves each child's durable mode/label from the registered `subagent` - * projection unit down a three-rung ladder — the registry's watermark - * snapshot for a live child; for a cold one, a durable projection-cache - * row when the optional cache serves an own-suffix identity (its `seq` - * gate proves the value postdates the fork seed, where a child's own - * descriptor is immutable once appended), else one persistence inspection - * folded through the registry. The - * projection fold is the single classification authority; per-child - * diagnostics relay a fold that served no identity or a failed inspection, - * never a list-time descriptor parse. Absent persistence, enumeration is - * live-only (a cold child cannot be resumed then either, so its absence is - * capability absence, not an error). This service consults no Agent - * registrations, Activations, or providers. - * - * Every persistence read receives `signal`, and the listing rechecks - * cancellation around each of those awaits. Read rejections that settle - * after an abort become a stable `SubagentError` with code `CANCELLED`. - * @param parentSessionId - parent session whose direct children are listed. - * @param signal - caller-owned cancellation forwarded to persistence reads - * and observed around every read await. - * @returns children and per-child diagnostics ordered by `createdAt`, then id. - * @throws {@link SubagentError} when the projection registry or the session - * store is not mounted, or the caller cancels the listing. - */ -listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise - -/** - * Enumerate the root's complete session-backed subagent tree in stable - * pre-order from one live-preferred corpus, without loading or resuming an - * Agent. Ordinary sessions and one-shot children remain traversal nodes so - * continuable descendants below them are discovered; each returned entry - * adds its durable `parentId` and root-relative `depth`. Identity resolution, - * diagnostics, optional persistence, and cancellation follow the same - * projection-backed contract as {@link listChildren}. - * @param rootSessionId - session whose complete descendant tree is listed. - * @param signal - caller-owned cancellation forwarded to persistence reads - * and observed around every read await. - * @returns children and per-candidate diagnostics with tree position, in - * stable pre-order. - * @throws {@link SubagentError} under the same conditions as {@link listChildren}. - */ -listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise - -/** - * Register a provider under its name. Registration is effect-scoped and HMR - * safe; removing a provider blocks new starts but does not revoke runs that - * were already returned to their holders. - * @param provider - the trusted provider implementation. - * @returns the exact Cordis effect disposer. - */ -registerProvider(provider: SubagentProvider): () => void - -/** - * Look up a provider by name. - * @param name - the provider name. - * @returns the provider, or undefined when absent. - */ -getProvider(name: string): SubagentProvider | undefined - -/** - * List registered provider names in insertion order. - * @returns the registered names. - */ -list(): string[] - -/** - * Establish a published child on the named provider. Capability and semantic - * checks run before delegation. Provider ownership lasts until its promise - * fulfills; a rejection therefore has no run for the caller to dispose and - * emits no run lifecycle events. Post-publication turn and infrastructure - * failures settle through the returned run. - * @param name - the provider to use. - * @param request - child label, prompt, parent, signal, and optional capabilities. - * @returns the published holder-owned run. - */ -async start(name: string, request: SubagentStartRequest): Promise -``` - -Types: [Agent](../subsystems/core.md) · [ContentBlock](../subsystems/core.md) · [ContinuableSetupContribution](../subsystems/subagent.md) · [ContinuableStart](../subsystems/subagent.md) · [ContinuableStartSpec](../subsystems/subagent.md) · [MessageId](../subsystems/core.md) · [SessionId](../subsystems/core.md) · [SubagentDescendantListEntry](../subsystems/subagent.md) · [SubagentFollowupOptions](../subsystems/subagent.md) · [SubagentInterruptAuthority](../subsystems/subagent.md) · [SubagentListEntry](../subsystems/subagent.md) · [SubagentProvider](../subsystems/subagent.md) · [SubagentReportOptions](../subsystems/subagent.md) · [SubagentRun](../subsystems/subagent.md) · [SubagentStartRequest](../subsystems/subagent.md) - -Source: [`packages/subagent/subagent/src/index.ts:167`](../../packages/subagent/subagent/src/index.ts) - -## `ctx.subprocess` — `SubprocessService` (abstract seam) - -Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). - -Implementations must honor these semantics: - -- Executable paths belong to one execution world shared with the mounted filesystem provider. -- spawn returns immediately with a live handle; `done` resolves at process close with exit facts and rejects only for spawn-level failures. -- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here. -- SubprocessHandle.terminate (and the spec's abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — tree-scoped on every platform. SubprocessHandle.waitForExit observes whole-tree liveness, so a consumer-owned teardown ladder can hold each tier on real quiescence. -- Disposal of the service terminates all still-running managed processes and awaits their exit. -- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits. - -```ts cordis-catalog -/** - * Resolve one configured executable in this provider's execution world. - * Absolute paths are verified; bare names use the provider's scrubbed PATH - * plus explicit environment overrides. Relative paths containing separators - * are rejected: no current consumer defines which directory they would - * resolve against, so providers fail loud instead of guessing. - * @param command - absolute executable path or bare PATH name. - * @param env - explicit environment entries used for lookup. - * @param signal - aborts remote or local lookup. - * @returns a canonical executable path. - */ -abstract resolveExecutable( command: string, env?: Readonly>, signal?: AbortSignal, ): Promise - -/** - * Start one managed child process from a fully-specified spec; this seam - * applies no defaults. - * @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment. - * @returns the live process handle (streams/readers, signalling, outcome promise). - */ -abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle - -/** - * Allocate a real terminal and start one owned process session. This is the - * only non-pipe process primitive: implementations own terminal byte I/O, - * foreground groups, signals, and complete session-tree cleanup. - * @param spec - fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation. - * @returns the live terminal handle after allocation succeeds. - */ -abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise -``` - -Types: [SubprocessHandle](../subsystems/subprocess.md) · [SubprocessSpawnSpec](../subsystems/subprocess.md) · [SubprocessTerminalHandle](../subsystems/subprocess.md) · [SubprocessTerminalSpawnSpec](../subsystems/subprocess.md) - -Source: [`packages/subprocess/subprocess/src/index.ts:102`](../../packages/subprocess/subprocess/src/index.ts) - -## `ctx.systemPrompt` — `SystemPrompt` - -Registry service for the prompt inputs assembled before each model step. - -```ts cordis-catalog -/** - * Register an ordered prompt section in the calling context's scope. A scoped - * section shadows a global section with the same name; duplicates within one - * layer and non-finite orders throw. Registration and disposal emit - * `system-prompt/change`. - * @param section - the section to register. - * @returns the exact Cordis effect disposer. - */ -section(section: PromptSection): () => void - -/** - * Register ordered dynamic context in the calling context's scope. Scoped - * entries shadow global entries with the same name. - * @param context - the context contribution to register. - * @returns the exact Cordis effect disposer. - */ -context(context: PromptContext): () => void - -/** - * Register a tool-schema provider in the calling context's scope. Global and - * matching scoped providers both contribute; returning the reserved - * {@link TOOL_ORDER_REST} name makes assembly fail. - * @param provider - evaluated for each assembly with its context. - * @returns the exact Cordis effect disposer. - */ -tools(provider: (context: AssembleContext) => ToolProviderResult): () => void - -/** - * Register a prompt variable in the calling context's scope. Scoped values - * shadow globals; invalid or duplicate names throw. A provider may return - * `undefined`, but rendering a section that references that value then fails. - * @param name - the `[a-z][a-z0-9_]*` reference name. - * @param provider - evaluated for each assembly. - * @returns the exact Cordis effect disposer. - */ -variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void - -/** - * Assemble global and scoped providers, detach tool parameters, apply - * canonical ordering, then run the assembly waterfall. Scoped sections and - * variables shadow globals; the returned waterfall value is authoritative. - * @param context - the optional scope and plugin-defined assembly fields. - * @returns the authoritative post-waterfall assembly. - */ -async assemble(context: AssembleContext = {}): Promise -``` - -Types: [AssembleContext](../subsystems/system-prompt.md) · [PromptContext](../subsystems/system-prompt.md) · [PromptSection](../subsystems/system-prompt.md) · [ToolProviderResult](../subsystems/system-prompt.md) - -Source: [`packages/core/system-prompt/src/index.ts:314`](../../packages/core/system-prompt/src/index.ts) - -## `ctx.tasks` — `TaskService` (abstract seam) - -Abstract background task registry. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.tasks` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). - -Implementations must honor these semantics: - -- Registrations outlive producer and control-surface fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. -- Owned-task access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary. -- Settlement is first-wins: one terminal record, one round of contained listener notification, and released waiters, even against a late producer outcome. -- start refuses work while no control surface is attached, so a producer cannot start work that callers cannot collect or stop. - -```ts cordis-catalog -/** - * Preflight access, validation, and owner cleanup before starting and - * atomically registering work. A throwing starter leaves nothing registered; - * after it returns, registration cannot fail. Settlement records the outcome, - * notifies listeners, and releases waiters. - * @param spec - task identity, owner, and synchronous starter. - * @returns the registry-issued `-N` id. - */ -abstract start(spec: TaskStart): TaskId - -/** - * List caller-owned and unowned tasks in registration order without exposing - * another session's labels. - * @param caller - reading agent; a non-agent caller sees only unowned tasks. - * @returns fresh snapshots. - */ -abstract list(caller?: Agent): TaskSnapshot[] - -/** - * Return a non-consuming snapshot without changing its read cursor or notice - * state. Throws for an unknown or foreign task. - * @param id - task to look up. - * @param caller - reading agent checked against the owner. - * @returns a fresh snapshot. - */ -abstract get(id: TaskId, caller?: Agent): TaskSnapshot - -/** - * Read the next stream delta, or the idempotent final output after settlement. - * A terminal read marks the task reported. Throws for an unknown or foreign - * task. - * @param id - task to read. - * @param caller - reading agent checked against the owner. - * @returns output text and the post-read snapshot. - */ -abstract read(id: TaskId, caller?: Agent): TaskRead - -/** - * Request cancellation, then mark the task stopping and reported. A producer - * throw propagates without changing task state. Throws for an unknown or - * foreign task. - * @param id - task to cancel. - * @param caller - killing agent checked against the owner. - * @param reason - logged reason forwarded to the producer. - * @returns `requested` for live work, otherwise `already-finished`. - */ -abstract kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished' - -/** - * Wait for settlement or timeout without cancelling the task. Caller abort - * rejects only while the task is live; after settlement the terminal - * snapshot wins so a notice suppressed for this waiter is still delivered. - * Throws for invalid, unknown, or foreign input. - * @param id - task to wait for. - * @param timeoutMs - positive finite wait bound in milliseconds. - * @param caller - waiting agent checked against the owner. - * @param signal - optional cancellation of the wait itself. - * @returns snapshot at settlement or timeout. - */ -abstract wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise - -/** - * Register an effect-scoped completion listener. Each listener is contained; - * returned promises are observed but not awaited. No listener runs after - * service disposal. - * @param listener - receives each terminal snapshot and its exact owner. - * @returns disposer that unregisters the listener. - */ -abstract onTaskDone(listener: TaskDoneListener): () => void - -/** - * Attach an effect-scoped surface that can read and stop tasks. {@link start} - * refuses work while none is attached. - * @param name - diagnostic label; duplicate names remain independent. - * @returns disposer that detaches this surface. - */ -abstract attachSurface(name: string): () => void -``` - -Types: [Agent](../subsystems/core.md) · [TaskDoneListener](../subsystems/tasks.md) · [TaskId](../subsystems/tasks.md) · [TaskRead](../subsystems/tasks.md) · [TaskSnapshot](../subsystems/tasks.md) · [TaskStart](../subsystems/tasks.md) - -Source: [`packages/tasks/tasks/src/index.ts:50`](../../packages/tasks/tasks/src/index.ts) - -## `ctx.telemetry` — `Telemetry` (abstract seam) - -The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. - -```ts cordis-catalog -/** - * See {@link TelemetryBackend.emit} — the seam declaration is the contract's one home. - * @param record - the logical record to report; owned by the backend after the call. - */ -abstract emit(record: TelemetryRecord): void - -/** See {@link TelemetryBackend.flush}. */ -flush?(): void - -/** - * See {@link TelemetryBackend.shutdown}. - * @returns resolves when the backend's pipeline has quiesced. - */ -abstract shutdown(): Promise -``` - -Types: [TelemetryRecord](../subsystems/telemetry.md) - -Source: [`packages/session/session-telemetry/src/index.ts:140`](../../packages/session/session-telemetry/src/index.ts) - -## `ctx.tokenMeter` — `TokenMeterService` - -Replay owner for one service-wide estimator and isolated per-session folds. - -```ts cordis-catalog -/** - * Measure current request pressure and surface through the durable tail. - * - * Provider usage is reused only when the latest successful call's canonical - * request envelope matches `requestHeader` and its total is no lower than - * that call's full heuristic anchor; otherwise the complete envelope and - * surface are heuristically repriced. - * - * `requestHeader` affects request pressure only; surface fields always - * describe the current session surface. Every call clones those positional - * nodes, so measurement is O(surface). - * - * @param session - session to replay through its current durable tail. - * @param requestHeader - optional effective request envelope replacing the latest logged header. - * @returns a detached deeply immutable pressure and surface measurement. - */ -measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement - -/** - * Heuristically price one model-visible message (instance face of the pure - * `estimateMessage` export from `estimate.ts`). - * @param message - message to price without mutation. - * @returns content and role-framing tokens under the fixed service heuristic. - */ -estimateMessage(message: Message): number -``` - -Types: [EpochHeader](../subsystems/session.md) · [Message](../subsystems/core.md) · [Session](../subsystems/session.md) · [TokenMeasurement](../subsystems/token-meter.md) - -Source: [`packages/llm/token-meter/src/index.ts:74`](../../packages/llm/token-meter/src/index.ts) - -## `ctx.toolResultPrune` — `ToolResultPruneService` - -Deterministic head/middle/tail pruning for current tool-result surface nodes. - -```ts cordis-catalog -/** - * Measure text content in Unicode code points; non-text blocks cost zero. - * @param blocks - tool-result content to measure. - * @returns total Unicode code points across text blocks. - */ -measureContent(blocks: readonly ContentBlock[]): number - -/** - * Replace an over-budget text middle while retaining rich-block order. - * Text slicing is by Unicode code point, not UTF-16 code unit, so a retained - * boundary cannot split a surrogate pair. Grapheme clusters may still split. - * @param blocks - original tool-result content. - * @returns pruned content, or `null` when the text is within budget. - */ -pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null - -/** - * Prune every over-budget tool result from one stable current-surface snapshot. - * Each replacement preserves the complete event data except for `content`, - * points at the shadowed node for durable provenance and replay, and is - * immediately preceded by a `compact/prune` shadow-price event pricing the - * shadowed node through the injected token meter, so pure consumers can - * subtract it without per-node state. - * @param session - session whose current surface is rewritten. - * @returns landed replacements and aggregate Unicode-code-point savings. - * @throws when the session rejects a replacement; replacements committed - * earlier in the pass remain durable. - */ -pruneSession(session: Session): PruneResult -``` - -Types: [ContentBlock](../subsystems/core.md) · [PruneResult](../subsystems/compaction.md) · [Session](../subsystems/session.md) - -Source: [`packages/compact/compact-tool-result-prune/src/index.ts:44`](../../packages/compact/compact-tool-result-prune/src/index.ts) - -## `ctx.tools` — `ToolRegistry` - -Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch. - -```ts cordis-catalog -/** - * Register globally or in the calling agent scope. Scoped tools shadow - * globals; duplicates within one layer and the reserved `run_code` name fail. - * @param definition - tool schema, execution, and optional finalization/presentation callbacks. - * @returns the exact disposer that unregisters the tool. - */ -register(definition: ToolDefinition): () => void - -/** - * Restrict global tools for the calling agent scope. Empty filters, unknown - * names, scope-local names, and reserved transport names fail. Restrictions - * intersect; scoped registrations remain visible. - * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove). - * @returns the exact disposer that lifts this restriction. - */ -restrict(filter: ToolRestriction): () => void - -/** - * Register a monotonic guard after the extensible `tools/pre-execute` - * waterfall. A plain-context guard applies globally; one registered through - * `agent.ctx` applies only to that agent. Any matching guard may deny by - * returning a reason, while no guard can force-allow a call another guard - * denied. The exact effect disposer is returned for ordered ownership and - * HMR cleanup. - * @param guard - synchronous check; a returned string denies the execution. - * @returns the exact disposer that unregisters the guard. - */ -guard(guard: ToolGuard): () => void - -/** - * Look up a tool as one scope sees it (scoped - * shadows global; a restricted-away global reads as absent). Presenters pass - * the calling agent so the rendered card matches the definition that - * actually executed. - * @param name - the tool name as registered. - * @param scope - the viewing scope (the agent); omitted = the global view. - * @returns the definition the scope resolves, or undefined when none is visible. - */ -get(name: string, scope?: ScopeKey): ToolDefinition | undefined - -/** - * Project visible definitions onto the allowlisted model-facing schema fields, - * excluding execution and presentation callbacks. - * @param scope - the viewing scope (the agent); omitted = the global view. - * @returns one deep-cloned schema per visible tool. - */ -schemas(scope?: ScopeKey): ToolSchema[] - -/** - * Classify a pending call through the caller's visible tool definition. Only - * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or - * throwing classifiers are exclusive. - * @param exec - call name, parsed arguments, and optional agent scope. - * @returns the fail-closed scheduling mode. - */ -executionMode(exec: ToolExecutionInput): ToolExecutionMode - -/** - * Execute through pre-policy, guards, around-dispatch, post-policy, - * definition-owned content finalization, and final notification. Tool and - * listener failures resolve as materialized error results; an invisible tool - * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen - * snapshot final observers receive. Cancellation - * arriving after entry and before final result materialization skips a - * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a - * successful started outcome with `ABORTED`; already-started work is still - * drained and may retain a tool-owned structured error. - * @param exec - the typed same-process call input. The registry assigns its - * correlation token before policy begins. - * @returns the materialized final result. - */ -async execute(exec: ToolExecutionInput): Promise -``` - -Types: [ScopeKey](../subsystems/scope.md) · [ToolDefinition](../subsystems/tools.md) · [ToolExecutionInput](../subsystems/tools.md) · [ToolExecutionMode](../subsystems/tools.md) · [ToolExecutionResult](../subsystems/tools.md) · [ToolGuard](../subsystems/tools.md) · [ToolRestriction](../subsystems/tools.md) · [ToolSchema](../subsystems/tools.md) - -Source: [`packages/core/tools/src/index.ts:739`](../../packages/core/tools/src/index.ts) - -## `ctx.typert` — `TypertRegistry` - -Registry of generated schemas, package reflection, invocations, and Remote dependency providers. - -```ts cordis-catalog -/** - * Register one generated contribution atomically for the calling fiber. - * Duplicate package-face identities, schemas, invocation ids, or endpoints - * reject the whole batch. - * @param contribution - generated schemas, reflection, and Host invocations. - * @returns the exact effect disposer that removes this contribution. - */ -register(contribution: TypertContribution): TypeRTDisposer - -/** - * Look up one schema by `#`. - * @param key - global schema key. - * @returns the live schema record, or `undefined` when absent. - */ -get(key: string): TypertSchemaRecord | undefined - -/** - * Resolve one required schema. - * @param key - global schema key. - * @returns the live schema record. - * @throws when the key is malformed, the package face is absent, or the schema is not contributed. - */ -resolve(key: string): TypertSchemaRecord - -/** - * Enumerate live schemas in registration order. - * @param filter - optional package and face restriction. - * @returns matching schema records. - */ -list(filter: TypertSchemaFilter = {}): TypertSchemaRecord[] - -/** - * Look up generated reflection for one package face. - * @param packageName - exact npm package name. - * @param face - face to query; defaults to the host runtime. - * @returns the live package record, or `undefined` when absent. - */ -getPackage(packageName: string, face: TypertFace = 'host'): TypertPackageRecord | undefined - -/** - * Enumerate generated package reflection in registration order. - * @param filter - optional package and face restriction. - * @returns matching package records. - */ -listPackages(filter: TypertPackageFilter = {}): TypertPackageRecord[] - -/** - * Project a live Zod schema to JSON Schema without caching the result. - * @param key - global schema key. - * @param params - Zod projection parameters. - * @returns a fresh JSON Schema document. - */ -toJSONSchema(key: string, params?: z.core.ToJSONSchemaParams): z.core.JSONSchema.BaseSchema -``` - -Source: [`packages/typert/registry/src/service.ts:446`](../../packages/typert/registry/src/service.ts) - -## `ctx.typertGateway` — `TypertGatewayService` - -Resolve strict generated definitions or conservative SRC markers against current Cordis Services and TypeRT providers. - -```ts cordis-catalog -/** - * Invoke one live Remote method through strict generated reflection or SRC markers. - * @param request - decoded endpoint and exact named wire arguments. - * @returns the validated business result. - * @throws {@link TypertGatewayError} for dispatch, provider, or boundary failures; lookup-policy and business errors retain identity. - */ -async invoke(request: InvokeRemoteRequest): Promise -``` - -Source: [`packages/api/gateway/src/index.ts:78`](../../packages/api/gateway/src/index.ts) - -## `ctx.userInteraction` — `UserInteractionService` - -`ctx.userInteraction`: one active UI provider plus an `ask()` surface. - -```ts cordis-catalog -/** - * Register the UI provider. Only one provider may be active in a context. - * - * @param provider UI-side implementation that collects answers. - * @returns Disposer that unregisters this provider. - */ -registerProvider(provider: UserInteractionProvider): () => void - -/** - * Ask the active UI provider and wait for the user's answer. - * - * When a caller supplies an agent, human interaction is valid only for the - * exact live runtime root. Runtime ownership, not durable session lineage, - * decides this boundary: an owned child has no human answerer and would - * block forever, while a lineage-bearing session resumed as a new runtime - * root may ask normally. - * - * @param request Questions, owner agent, and abort signal. - * @returns The answer chosen or typed by the human. - * @throws {UserInteractionError} code `CALLER_NOT_LIVE` when a supplied - * agent is not the registry's exact live instance, or `DELEGATED_CALLER` - * when that live agent is owned by another agent. - */ -async ask(request: AskUserQuestionRequest): Promise -``` - -Types: [AskUserQuestionAnswer](../subsystems/user-interaction.md) · [AskUserQuestionRequest](../subsystems/user-interaction.md) · [UserInteractionProvider](../subsystems/user-interaction.md) - -Source: [`packages/interaction/user-interaction/src/index.ts:51`](../../packages/interaction/user-interaction/src/index.ts) - -## `ctx.web` — `WebService` - -The web access service. Registered as `ctx.web` (one instance per context). - -Selection semantics (resolved at execution time, never order-dependent): - -- A configured id that is registered and `available()` → that provider. -- A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`. -- A configured id registered but unavailable → `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. -- No id configured, exactly one registered usable provider → that provider. -- No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`. -- No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`. - -```ts cordis-catalog -/** - * Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` - * if its id is already registered for search. Returns a disposer; disposed - * with the calling fiber. - * @param provider - the provider; its `id` is the registry key. - * @returns the disposer that unregisters the provider. - */ -registerSearchProvider(provider: WebSearchProvider): () => void - -/** - * Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` - * if its id is already registered for fetch. Returns a disposer; disposed - * with the calling fiber. - * @param provider - the provider; its `id` is the registry key. - * @returns the disposer that unregisters the provider. - */ -registerFetchProvider(provider: WebFetchProvider): () => void - -/** - * Run one search through the selected provider. Resolves the provider at call - * time with the selection rules above; throws {@link WebError} when the - * capability cannot run. The seam enforces `request.maxResults` on the result: - * if the provider over-returns, `sources[]` is truncated and `truncated` set. - * @param request - the query plus result-shaping options. - * @param signal - optional cancellation signal forwarded to the provider. - * @returns the provider's results, capped to `request.maxResults`. - */ -async search(request: WebSearchRequest, signal?: AbortSignal): Promise - -/** - * Retrieve one URL through the selected provider. Resolves the provider at - * call time with the selection rules above; throws {@link WebError} when the - * capability cannot run. A non-2xx response is a result, not a throw. - * @param request - the URL plus retrieval options. - * @param signal - optional cancellation signal forwarded to the provider. - * @returns the retrieval outcome; non-2xx responses resolve descriptively. - */ -async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise -``` - -Types: [WebFetchProvider](../subsystems/web.md) · [WebFetchRequest](../subsystems/web.md) · [WebFetchResult](../subsystems/web.md) · [WebSearchProvider](../subsystems/web.md) · [WebSearchRequest](../subsystems/web.md) · [WebSearchResult](../subsystems/web.md) - -Source: [`packages/web/web/src/index.ts:74`](../../packages/web/web/src/index.ts) - -## `ctx.workflows` — `WorkflowService` (abstract seam) - -Workflow execution seam. Invalid requests throw before publication; a live run is holder-owned, its result never rejects, cancellation and disposal are bounded, and disposal waits for child cleanup within that bound. Lifecycle listener failures are contained, and `workflow/end` fires exactly once as the result settles. - -```ts cordis-catalog -/** - * Parse and execute a workflow script. - * @param request - the script, its `args`, the parent agent, and an - * optional cancel signal. - * @returns the live run; its `result` resolves when the script settles. - */ -abstract start(request: WorkflowStartRequest): WorkflowRun -``` - -Types: [WorkflowRun](../subsystems/workflow.md) · [WorkflowStartRequest](../subsystems/workflow.md) - -Source: [`packages/workflow/workflow/src/index.ts:159`](../../packages/workflow/workflow/src/index.ts) - -## `ctx.workspace` — `WorkspaceRegistry` - -Durable workspace registry. Startup waits for `sessionPersistence`, builds one canonical-cwd header index, and completes the one-time history bootstrap before the service becomes active. The persistence dependency is mandatory so an unavailable peer can never be mistaken for an empty history and commit the initialized marker. - -```ts cordis-catalog -/** - * Create or reuse a workspace for an existing directory. The path is - * canonicalized through `fs.realpath`; a nonexistent path rejects with the - * original error and a non-directory rejects. Repeated calls for the same - * canonical path return the existing entity without changing its title. - * A newly created workspace is prepended to the durable registry order. - * Different canonical paths may share a display title. - * @param path - Existing directory to own, in any path spelling. - * @param title - Display title used only when a new record is created. - * @returns the existing or newly durable workspace. - */ -async create(path: string, title?: string): Promise - -/** - * Look up a workspace by id. - * @param id - Workspace id. - * @returns the workspace, or `undefined` when unknown. - */ -get(id: WorkspaceId): Workspace | undefined - -/** - * Synchronous workspace projection in durable registry order. Every - * entity's `sessionIds` getter is already filtered by the startup/live - * canonical-cwd header index; this method performs no persistence reads. - * @returns a fresh ordered array of workspace entities. - */ -list(): Workspace[] - -/** - * Delete one workspace registration while retaining its directory and every - * session log. The durable order is updated before the table deletion; a - * failed table write restores the prior order and keeps the entity - * published. Unknown ids are an idempotent no-op for domain callers. - * @param id - Workspace registration to remove. - * @returns `true` when a record was deleted, `false` when it was unknown. - */ -delete(id: WorkspaceId): Promise - -/** - * Archive one session durably. The session must exist (live or in session - * persistence); its workspace accounting — or lack of one — is irrelevant. - * An already archived id resolves without writing. - * @param sessionId - The session to archive. - * @returns resolution after durability. - */ -archiveSession(sessionId: SessionId): Promise - -/** - * Resolve by canonical directory path without creating or mutating a - * workspace. A missing path rejects during `realpath`; an existing unowned - * directory returns `undefined`. - * @param path - Existing directory path in any spelling. - * @returns the workspace owning the canonical path, when one exists. - */ -async resolveByPath(path: string): Promise -``` - -Types: [SessionId](../subsystems/core.md) · [Workspace](../subsystems/workspace.md) · [WorkspaceId](../subsystems/workspace.md) - -Source: [`packages/workspace/workspace/src/index.ts:81`](../../packages/workspace/workspace/src/index.ts) - -## Inherited `ctx` members (cordis core + loader/hmr/timer) - -The framework `ctx` surface every plugin also sees, beyond the harness services above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of what `ctx` offers, without elevating framework internals to the harness tier's prominence. - -- `ctx.on / ctx.once` — Register an event listener (disposable). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts)) -- `ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall` — Dispatch an event (sync / awaited / first-bail / veto-chain). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts)) -- `ctx.plugin / ctx.inject` — Load a plugin / declare required services. ([`vendor/cordis/src/registry.ts:164`](../../vendor/cordis/src/registry.ts)) -- `ctx.effect` — Register a disposable side effect tied to the fiber. ([`vendor/cordis/src/fiber.ts:9`](../../vendor/cordis/src/fiber.ts)) -- `ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin` — Low-level service-store access and binding. ([`vendor/cordis/src/reflect.ts:7`](../../vendor/cordis/src/reflect.ts)) -- `ctx.extend / ctx.isolate / ctx.intercept` — Derive a child context (scoped services / isolation / interception). ([`vendor/cordis/src/context.ts:42`](../../vendor/cordis/src/context.ts)) -- `ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger` — Ambient handles onto the running context graph. ([`vendor/cordis/src/context.ts:16`](../../vendor/cordis/src/context.ts)) -- `ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)` — Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick). ([`vendor/timer/src/index.ts:4`](../../vendor/timer/src/index.ts)) -- `ctx.loader` — The config Loader that booted the app (present under the loader). ([`vendor/loader/src/index.ts:30`](../../vendor/loader/src/index.ts)) -- `ctx.hmr` — The hot-module-reload watcher (present under the hmr plugin). ([`vendor/hmr/src/index.ts:15`](../../vendor/hmr/src/index.ts)) diff --git a/docs/cordis-primer.i18n.yaml b/docs/cordis-primer.i18n.yaml index 856ea2302b..ec67982eb5 100644 --- a/docs/cordis-primer.i18n.yaml +++ b/docs/cordis-primer.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/cordis-primer.md -cordis-primer.md: ee65e6e702ecaeb506ce7334032c38e09c936cda -cordis-primer.zh.md: 051dd7c956a4db107a4ef7d1414435f9c2b2603d +cordis-primer.md: 4bcb2c9979994ca70f92031cbdc5dd22df9c1977 +cordis-primer.zh.md: d84beac54463ec193f009f506a4c9b1bda02c243 diff --git a/docs/cordis-primer.md b/docs/cordis-primer.md index ee65e6e702..4bcb2c9979 100644 --- a/docs/cordis-primer.md +++ b/docs/cordis-primer.md @@ -2,7 +2,7 @@ English | [中文](cordis-primer.zh.md) -Cordis is the vendored plugin framework underneath the DeepSeek Harness SDK. This primer teaches the Cordis ideas a harness plugin author needs before reading the generated [events](cordis-catalog/events.md) and [services](cordis-catalog/services.md) catalogs; the [Cordis tutorial](cordis-tutorial/index.md) walks the same ideas hands-on. The vendored source and sync procedure live in [vendor/README.md](../vendor/README.md). +Cordis is the vendored plugin framework underneath the DeepSeek Harness SDK. This primer teaches the Cordis ideas a harness plugin author needs before reading the generated service/event reference on the [subsystem pages](subsystems/core.md); the [Cordis tutorial](cordis-tutorial/index.md) walks the same ideas hands-on. The vendored source and sync procedure live in [vendor/README.md](../vendor/README.md). ## Cordis In Five Ideas diff --git a/docs/cordis-primer.zh.md b/docs/cordis-primer.zh.md index 051dd7c956..d84beac544 100644 --- a/docs/cordis-primer.zh.md +++ b/docs/cordis-primer.zh.md @@ -2,7 +2,7 @@ [English](cordis-primer.md) | 中文 -Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。本文介绍 harness 插件作者在阅读生成的[事件](cordis-catalog/events.md)与[服务](cordis-catalog/services.md)目录之前需要了解的 Cordis 核心概念;[Cordis 教程](cordis-tutorial/index.md)则通过实践逐一讲解这些概念。vendor 源码与同步流程见 [vendor/README.md](../vendor/README.md)。 +Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。本文介绍 harness 插件作者在阅读[子系统页面](subsystems/core.md)上生成的服务/事件参考之前需要了解的 Cordis 核心概念;[Cordis 教程](cordis-tutorial/index.md)则通过实践逐一讲解这些概念。vendor 源码与同步流程见 [vendor/README.md](../vendor/README.md)。 ## 五个核心概念 diff --git a/docs/cordis-tutorial/03-services.i18n.yaml b/docs/cordis-tutorial/03-services.i18n.yaml index 2849ed8858..bdb7e19387 100644 --- a/docs/cordis-tutorial/03-services.i18n.yaml +++ b/docs/cordis-tutorial/03-services.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/cordis-tutorial/03-services.md -03-services.md: 562b49ede0aa4cc1d58c4d6af7c7d5d1ebb2e4b1 -03-services.zh.md: 964f7e3654614d136b8765bb727f85a5a05587a8 +03-services.md: 82b08b7b8a2ec8a6b340dd1fdc7fa3de98cedff9 +03-services.zh.md: ba4152454eb79a21b183b867c0ba2ef32cd43923 diff --git a/docs/cordis-tutorial/03-services.md b/docs/cordis-tutorial/03-services.md index 562b49ede0..82b08b7b8a 100644 --- a/docs/cordis-tutorial/03-services.md +++ b/docs/cordis-tutorial/03-services.md @@ -91,7 +91,7 @@ export function apply(ctx: Context) { ## Naming -Service names live in one flat namespace per application. Prefix or namespace your own services distinctively (the harness claims plain names like `tools` and `llm`); the generated [services catalog](../cordis-catalog/services.md) lists every name the harness registers. +Service names live in one flat namespace per application. Prefix or namespace your own services distinctively (the harness claims plain names like `tools` and `llm`); the generated `cordis-surface` regions on the [subsystem pages](../subsystems/core.md) list every name the harness registers. Next: [Events](04-events.md) — communication without a shared service. diff --git a/docs/cordis-tutorial/03-services.zh.md b/docs/cordis-tutorial/03-services.zh.md index 964f7e3654..ba4152454e 100644 --- a/docs/cordis-tutorial/03-services.zh.md +++ b/docs/cordis-tutorial/03-services.zh.md @@ -91,7 +91,7 @@ export function apply(ctx: Context) { ## 命名 -每个应用中的服务名称共用一个扁平命名空间。请为自有服务添加有辨识度的前缀或命名空间(harness 已占用 `tools` 和 `llm` 等普通名称);生成的[服务目录](../cordis-catalog/services.md)列出 harness 注册的每个名称。 +每个应用中的服务名称共用一个扁平命名空间。请为自有服务添加有辨识度的前缀或命名空间(harness 已占用 `tools` 和 `llm` 等普通名称);[子系统页面](../subsystems/core.md)上生成的 `cordis-surface` 区块列出 harness 注册的每个名称。 下一章:[事件](04-events.md):无需共享服务即可通信。 diff --git a/docs/cordis-tutorial/04-events.i18n.yaml b/docs/cordis-tutorial/04-events.i18n.yaml index e7dc182114..a78b8f690c 100644 --- a/docs/cordis-tutorial/04-events.i18n.yaml +++ b/docs/cordis-tutorial/04-events.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/cordis-tutorial/04-events.md -04-events.md: 28ccb85d657afaabb5c6b4b1e9b10d6cf8710918 -04-events.zh.md: f78c971dcd9674d2a256c41000b627aecb2a572a +04-events.md: b0563c71fada3efc760ff7183bc7a82e7e86934e +04-events.zh.md: 0999cbf116eca452e54418132fbaefe3622b2c26 diff --git a/docs/cordis-tutorial/04-events.md b/docs/cordis-tutorial/04-events.md index 28ccb85d65..b0563c71fa 100644 --- a/docs/cordis-tutorial/04-events.md +++ b/docs/cordis-tutorial/04-events.md @@ -89,7 +89,7 @@ Because `ctx.on()` is an effect, the listener disappears with the plugin — no | bail | `ctx.bail(name, ...args)` | Synchronous version of serial. | | waterfall | `ctx.waterfall(name, ...args, next)` | Around-middleware; see below. | -Every harness event documents its mode in the generated [events catalog](../cordis-catalog/events.md). +Every harness event documents its mode in the generated reference on its owning [subsystem page](../subsystems/core.md). ## Waterfall: transform or short-circuit @@ -137,7 +137,7 @@ Walk through the second line: listener 1 runs first, calls `next()`, which invok The discipline that follows: **a waterfall listener that only observes or annotates must call `next()`**; returning without it is a deliberate short-circuit. Forgetting `next()` in a logging listener silently swallows the default behavior for everyone downstream. This is important enough that it is a standing rule of this repository ([waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)). -The harness uses waterfalls for decisions that cooperating plugins may wrap or answer: [`agent/request`](../cordis-catalog/events.md#agentrequest--waterfall) lets a plugin replace the model-call config, and [`approval/request`](../cordis-catalog/events.md#approvalrequest--waterfall) lets a policy answer instead of the user. +The harness uses waterfalls for decisions that cooperating plugins may wrap or answer: [`agent/request`](../subsystems/core.md#agentrequest--waterfall) lets a plugin replace the model-call config, and [`approval/request`](../subsystems/approval.md#approvalrequest--waterfall) lets a policy answer instead of the user. Next: [Configuration](05-config.md) — plugin options from `cordis.yml`. diff --git a/docs/cordis-tutorial/04-events.zh.md b/docs/cordis-tutorial/04-events.zh.md index f78c971dcd..0999cbf116 100644 --- a/docs/cordis-tutorial/04-events.zh.md +++ b/docs/cordis-tutorial/04-events.zh.md @@ -89,7 +89,7 @@ export function apply(ctx: Context) { | bail | `ctx.bail(name, ...args)` | serial 的同步版本。 | | waterfall(瀑布式事件) | `ctx.waterfall(name, ...args, next)` | 环绕中间件,见下文。 | -每个 harness 事件都会在生成的[事件目录](../cordis-catalog/events.md)中记录其模式。 +每个 harness 事件都会在其所属[子系统页面](../subsystems/core.md)的生成参考中记录其模式。 ## waterfall:转换或短路 @@ -137,7 +137,7 @@ HELLO 由此得到一项纪律:**只负责观察或标注的 waterfall 监听器必须调用 `next()`**;不调用就直接返回代表有意短路。如果日志监听器忘记调用 `next()`,会悄无声息地吞掉所有下游的默认行为。这一点极其重要,已成为本仓库的常设规则([waterfall 语义](../cordis-primer.md#cordis-waterfall-semantics))。 -harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`agent/request`](../cordis-catalog/events.md#agentrequest--waterfall) 允许插件替换模型调用配置,[`approval/request`](../cordis-catalog/events.md#approvalrequest--waterfall) 允许策略代替用户作答。 +harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`agent/request`](../subsystems/core.md#agentrequest--waterfall) 允许插件替换模型调用配置,[`approval/request`](../subsystems/approval.md#approvalrequest--waterfall) 允许策略代替用户作答。 下一章:[配置](05-config.md):来自 `cordis.yml` 的插件选项。 diff --git a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml b/docs/cordis-tutorial/07-into-the-harness.i18n.yaml index f3dde47f3a..fd29baab75 100644 --- a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml +++ b/docs/cordis-tutorial/07-into-the-harness.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/cordis-tutorial/07-into-the-harness.md -07-into-the-harness.md: e02f8f8d55b3fbe9087d46f8f50baeecb592c1c6 -07-into-the-harness.zh.md: 5f770267e8f3db04cd9cb0e92b6a6278cf05d5e4 +07-into-the-harness.md: 38483b5c4993a44562970782dca5f676e4cb84f6 +07-into-the-harness.zh.md: 59ce716bdace894682bc1c8e6e00cf174008c26c diff --git a/docs/cordis-tutorial/07-into-the-harness.md b/docs/cordis-tutorial/07-into-the-harness.md index e02f8f8d55..38483b5c49 100644 --- a/docs/cordis-tutorial/07-into-the-harness.md +++ b/docs/cordis-tutorial/07-into-the-harness.md @@ -101,7 +101,7 @@ Where to go next: - [Build a tool](../user/develop/basic/tool.md) — more of `defineTool`, including presentation and richer schemas. - [Three-layer capability design](../user/develop/practice/index.md) — how the harness structures replaceable capabilities. -- The generated [services](../cordis-catalog/services.md) and [events](../cordis-catalog/events.md) catalogs — everything you can inject and listen to. +- The generated `cordis-surface` regions on the [subsystem pages](../subsystems/core.md) — everything you can inject and listen to, each on its owning page. - [Architecture](../architecture.md) — the system map these plugins live in. [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk) diff --git a/docs/cordis-tutorial/07-into-the-harness.zh.md b/docs/cordis-tutorial/07-into-the-harness.zh.md index 5f770267e8..59ce716bda 100644 --- a/docs/cordis-tutorial/07-into-the-harness.zh.md +++ b/docs/cordis-tutorial/07-into-the-harness.zh.md @@ -101,7 +101,7 @@ logger 会先触发:`tools/result` 在结果物化过程中发出,发生在 - [构建工具](../user/develop/basic/tool.md):深入了解 `defineTool`,包括呈现和更丰富的 schema。 - [三层能力设计](../user/develop/practice/index.md):harness 如何组织可替换能力。 -- 生成的[服务](../cordis-catalog/services.md)与[事件](../cordis-catalog/events.md)目录:可以注入和监听的所有内容。 +- [子系统页面](../subsystems/core.md)上生成的 `cordis-surface` 区块:可以注入和监听的所有内容,各在其所属页面上。 - [架构](../architecture.md):这些插件所处的系统地图。 [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk) diff --git a/docs/cordis-tutorial/index.i18n.yaml b/docs/cordis-tutorial/index.i18n.yaml index 496a3fffa5..fa810d635f 100644 --- a/docs/cordis-tutorial/index.i18n.yaml +++ b/docs/cordis-tutorial/index.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/cordis-tutorial/index.md -index.md: a20976706f520416236ca759ee33649d1601eaa9 -index.zh.md: f6989521d4b7dffac6114867cc12371af4e4316f +index.md: 7a0bb6f8c736bf31d655a7763cfb7039c343d1a2 +index.zh.md: e6f6dc0cccef3f44273655b98b695bdc4632e95a diff --git a/docs/cordis-tutorial/index.md b/docs/cordis-tutorial/index.md index a20976706f..7a0bb6f8c7 100644 --- a/docs/cordis-tutorial/index.md +++ b/docs/cordis-tutorial/index.md @@ -6,7 +6,7 @@ Cordis is the plugin framework underneath the DeepSeek Harness SDK: a small runt The audience is agent developers. You do not need deep TypeScript experience; the [TypeScript notes](#typescript-notes) below explain the syntax that may be unfamiliar, and every chapter shows the exact commands and expected output. -If you want the condensed concept reference instead of a walkthrough, read the [Cordis primer](../cordis-primer.md). The exhaustive API reference lives in the generated [events](../cordis-catalog/events.md) and [services](../cordis-catalog/services.md) catalogs and the [Cordis core API](../cordis-catalog/core/context.md) pages. +If you want the condensed concept reference instead of a walkthrough, read the [Cordis primer](../cordis-primer.md). The exhaustive API reference lives in the generated `cordis-surface` regions on the [subsystem pages](../subsystems/core.md) and the [Cordis core API](../cordis-api/context.md) pages. ## Setup diff --git a/docs/cordis-tutorial/index.zh.md b/docs/cordis-tutorial/index.zh.md index f6989521d4..e6f6dc0ccc 100644 --- a/docs/cordis-tutorial/index.zh.md +++ b/docs/cordis-tutorial/index.zh.md @@ -6,7 +6,7 @@ Cordis 是 DeepSeek Harness SDK 底层的插件框架:它是一个小型运行 本教程面向 agent 开发者。你不需要深入掌握 TypeScript;下文的 [TypeScript 说明](#typescript-notes)会解释可能陌生的语法,并且每一章都会给出确切命令和预期输出。 -如果你想阅读精简的概念参考,而不是逐步实践,请参阅 [Cordis 入门](../cordis-primer.md)。详尽的 API 参考见生成的[事件](../cordis-catalog/events.md)与[服务](../cordis-catalog/services.md)目录,以及 [Cordis 核心 API](../cordis-catalog/core/context.md) 页面。 +如果你想阅读精简的概念参考,而不是逐步实践,请参阅 [Cordis 入门](../cordis-primer.md)。详尽的 API 参考见[子系统页面](../subsystems/core.md)上生成的 `cordis-surface` 区块,以及 [Cordis 核心 API](../cordis-api/context.md)页面。 ## 准备工作 diff --git a/docs/graph-atlas.md b/docs/graph-atlas.md index 0c955c9480..5b831520fb 100644 --- a/docs/graph-atlas.md +++ b/docs/graph-atlas.md @@ -3,7 +3,7 @@ # Documentation Graph Index -These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the generated [events](cordis-catalog/events.md) / [services](cordis-catalog/services.md) catalogs, [tool-catalog.md](tool-catalog.md), and [subsystems/](subsystems/core.md). +These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the [subsystem pages](subsystems/core.md) (types + the generated `cordis-surface` regions) and [tool-catalog.md](tool-catalog.md). The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md). diff --git a/docs/i18n/style-samples.md b/docs/i18n/style-samples.md index b9a330f21d..432c95e684 100644 --- a/docs/i18n/style-samples.md +++ b/docs/i18n/style-samples.md @@ -14,9 +14,9 @@ 依赖约束规范:各类扩展插件仅依赖抽象接口,严禁直接依赖 `dsh-agent-loop`(该主循环支持替换实现);唯一允许的特例是组合包 `dsh-agent-spine-demo`,它的职责是组装整套实体主干。 -> This document covers **behavior**; type shapes live in [subsystems/](../subsystems/core.md), the per-event/service reference in the [generated catalog](../cordis-catalog/events.md), per-package contracts in the package READMEs ([map](../../packages/README.md)). +> This document covers **behavior**; type shapes live in [subsystems/](../subsystems/core.md), the per-event/service reference in the generated regions of [subsystems/](../subsystems/core.md), per-package contracts in the package READMEs ([map](../../packages/README.md)). -本文档描述整体行为逻辑;类型定义存放于 [subsystems/](../subsystems/core.md);各类事件、服务的详细参考见[生成目录](../cordis-catalog/events.md);各包(package)的对外契约写在相应的 README 中([索引](../../packages/README.md))。 +本文档描述整体行为逻辑;类型定义存放于 [subsystems/](../subsystems/core.md);各类事件、服务的详细参考见 [subsystems/](../subsystems/core.md) 中的生成区块;各包(package)的对外契约写在相应的 README 中([索引](../../packages/README.md))。 ## ② 防御模式规则 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 04f992e536..04c3a492fe 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -3,7 +3,7 @@ # Session Persistence Event Catalog -Every event type that can appear in a session's durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](subsystems/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](subsystems/persistence.md) (how the log is made durable), and the [cordis events catalog](cordis-catalog/events.md) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit). +Every event type that can appear in a session's durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](subsystems/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](subsystems/persistence.md) (how the log is made durable), and the generated region of [session.md](subsystems/session.md#cordis-surface) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit). This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md). diff --git a/docs/subsystems/approval.i18n.yaml b/docs/subsystems/approval.i18n.yaml index a3447601a4..93c56707bf 100644 --- a/docs/subsystems/approval.i18n.yaml +++ b/docs/subsystems/approval.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/approval.md -approval.md: 8f17f98101950e6413d67fb4cce6befc5aede069 -approval.zh.md: c19285572b667512427b71d4af214acbc2baf334 +approval.md: 2045f2073b72fa265728cbf77b673053a2eaf371 +approval.zh.md: c021db14c4925acaeef5d984d9dfa88ad55bd8dd diff --git a/docs/subsystems/approval.md b/docs/subsystems/approval.md index 8f17f98101..2045f2073b 100644 --- a/docs/subsystems/approval.md +++ b/docs/subsystems/approval.md @@ -87,3 +87,85 @@ interface ApprovalRequest { `ctx.approval.request(req)` requires the requesting session to be inside an open turn. It appends `approval/asked`, obtains one outcome, appends the matching `approval/decided`, and resolves with that outcome. The `never` policy is enforced inside the service before waterfall dispatch, so even an answerer registered later with `prepend` cannot bypass it. Answerers return an outcome when they own the request or call `next()` to delegate; the first answer occupies the single decision slot. The audit events are log-only and do not enter the model transcript. Model-visible behavior is the caller's derived tool result plus the current runtime-context snapshot. Service disposal removes its context contribution; answerer listeners are independently effect-bound to their owning plugins. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.approval` — `ApprovalService` + +Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session. It exposes deterministic policy changes to the model through the runtime-context snapshot and switch notices. + +```ts cordis-catalog +/** + * Switch one live agent's policy and queue the transition for its next model + * step. Session initialization uses {@link setApprovalPolicy} directly + * because there is no previously visible policy to change. + * @param agent - the live agent whose policy is changing. + * @param policy - the new effective policy. + */ +setPolicy(agent: Agent, policy: ApprovalPolicy): void + +/** + * Ask the composed answerers to decide one readonly same-process request. + * The service borrows the request, agent, session, and live signal directly. + * The request requires an open turn because the audit pair must be enclosed + * by the durable log's commit/replay boundary; an idle ask rejects before + * appending anything. The answerer phase always produces an outcome: an + * aborted signal yields `'cancelled'`, a missing or throwing answerer yields + * `'unavailable'` (fail closed), and a rogue non-vocabulary return value is + * normalized to `'unavailable'`. A failure that prevents either audit append + * from committing still rejects because returning an unlogged decision would + * violate the pair. Session contains post-commit observer failures, so an + * authoritative append cannot reject the request or suppress its matching + * audit event. + * @param req - the pending decision (agent, tool identity, reason, signal). + * @returns the closed outcome; `'allowed-once'` is the only grant. + * @throws when no turn is open or either audit event fails before the session + * append commit point. + */ +async request(req: ApprovalRequest): Promise + +/** + * Read the session override without applying the configured default. + * @param session - session whose log supplies the override. + * @returns the last logged policy, or `undefined` without one. + */ +overrideOf(session: Session): ApprovalPolicy | undefined +``` + +Types: [Agent](core.md) · [Session](session.md) + +Source: [`packages/interaction/user-approval/src/index.ts:193`](../../packages/interaction/user-approval/src/index.ts) + + + +### `approval/*` events + + + +#### `approval/request` — waterfall + +Ask composed answerers for one decision. Return an outcome to claim the request or call `next()`; failure yields the fail-closed default. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + +```ts cordis-catalog +/** + * Ask composed answerers for one decision. Return an outcome to claim the + * request or call `next()`; failure yields the fail-closed default. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @param req - the pending decision (agent, tool identity, reason, signal). + * @mode waterfall + */ +'approval/request'(this: Scoped, req: ApprovalRequest, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/interaction/user-approval/src/index.ts:30`](../../packages/interaction/user-approval/src/index.ts) + diff --git a/docs/subsystems/approval.zh.md b/docs/subsystems/approval.zh.md index c19285572b..c021db14c4 100644 --- a/docs/subsystems/approval.zh.md +++ b/docs/subsystems/approval.zh.md @@ -87,3 +87,85 @@ interface ApprovalRequest { `ctx.approval.request(req)` 要求发起请求的会话处于一个尚未结束的轮次内。它追加 `approval/asked`,获取一个结果,追加对应的 `approval/decided`,然后以该结果完成。`never` 策略在服务内部、waterfall 分发之前强制执行,因此即使后来以 `prepend` 注册的应答者也无法绕过它。应答者在拥有该请求时返回结果,否则调用 `next()` 委托;第一个应答占据唯一的决策槽位。 审计事件仅写入日志,不进入模型 transcript(文本记录)。模型可见的行为是调用方派生的工具结果与当前运行时上下文快照。服务 dispose(资源释放)时会移除其上下文贡献;应答者监听器独立地通过 effect 绑定到其所属插件。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.approval` — `ApprovalService` + +Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session. It exposes deterministic policy changes to the model through the runtime-context snapshot and switch notices. + +```ts cordis-catalog +/** + * Switch one live agent's policy and queue the transition for its next model + * step. Session initialization uses {@link setApprovalPolicy} directly + * because there is no previously visible policy to change. + * @param agent - the live agent whose policy is changing. + * @param policy - the new effective policy. + */ +setPolicy(agent: Agent, policy: ApprovalPolicy): void + +/** + * Ask the composed answerers to decide one readonly same-process request. + * The service borrows the request, agent, session, and live signal directly. + * The request requires an open turn because the audit pair must be enclosed + * by the durable log's commit/replay boundary; an idle ask rejects before + * appending anything. The answerer phase always produces an outcome: an + * aborted signal yields `'cancelled'`, a missing or throwing answerer yields + * `'unavailable'` (fail closed), and a rogue non-vocabulary return value is + * normalized to `'unavailable'`. A failure that prevents either audit append + * from committing still rejects because returning an unlogged decision would + * violate the pair. Session contains post-commit observer failures, so an + * authoritative append cannot reject the request or suppress its matching + * audit event. + * @param req - the pending decision (agent, tool identity, reason, signal). + * @returns the closed outcome; `'allowed-once'` is the only grant. + * @throws when no turn is open or either audit event fails before the session + * append commit point. + */ +async request(req: ApprovalRequest): Promise + +/** + * Read the session override without applying the configured default. + * @param session - session whose log supplies the override. + * @returns the last logged policy, or `undefined` without one. + */ +overrideOf(session: Session): ApprovalPolicy | undefined +``` + +Types: [Agent](core.md) · [Session](session.md) + +Source: [`packages/interaction/user-approval/src/index.ts:193`](../../packages/interaction/user-approval/src/index.ts) + + + +### `approval/*` events + + + +#### `approval/request` — waterfall + +Ask composed answerers for one decision. Return an outcome to claim the request or call `next()`; failure yields the fail-closed default. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + +```ts cordis-catalog +/** + * Ask composed answerers for one decision. Return an outcome to claim the + * request or call `next()`; failure yields the fail-closed default. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @param req - the pending decision (agent, tool identity, reason, signal). + * @mode waterfall + */ +'approval/request'(this: Scoped, req: ApprovalRequest, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/interaction/user-approval/src/index.ts:30`](../../packages/interaction/user-approval/src/index.ts) + diff --git a/docs/subsystems/bash.i18n.yaml b/docs/subsystems/bash.i18n.yaml index 30e15dbacc..f6ed20cace 100644 --- a/docs/subsystems/bash.i18n.yaml +++ b/docs/subsystems/bash.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/bash.md -bash.md: f83a133c8e049bc111cfabd2b59def18a1b2d607 -bash.zh.md: 27ca9f6e5a8e05cd4ffa86e72d433b6f22005f09 +# pnpm run verify-translation-pairing --write docs/subsystems/bash.md +bash.md: dd7649ce46bea5642270fa553eb3d82f2dc33e9b +bash.zh.md: bba9ece2955cee66e941aa878299cff6c06a97a0 diff --git a/docs/subsystems/bash.md b/docs/subsystems/bash.md index f83a133c8e..dd7649ce46 100644 --- a/docs/subsystems/bash.md +++ b/docs/subsystems/bash.md @@ -219,3 +219,85 @@ interface BashProcessRead { ## The service `BashExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns command defaulting, timeout/abort classification, the terminal environment, and the background read merge; process groups, bounded collectors, spill files, credential scrubbing, and disposal quiescence are the [subprocess service](subprocess.md)'s. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic task runtime](tasks.md). `dsh-bash` owns the shell tools' shared exit-status contract: the exported `parseExitStatus`/`ParsedExitStatus` inverts the `[exit code: N]` / `[killed by signal: X]` markers `dsh-tool-bash`'s `renderResult` and `dsh-tool-pwsh`'s `renderPwshResult` append, and both tools' `presentResult` use it to split the rendered text into the terminal card's output body and its exit-status pill. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.bash` — `BashExecutor` (abstract seam) + +Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). + +Implementations must honor these semantics: + +- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult. +- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr. +- BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files. +- A still-running background process is stopped and awaited when its owning composition tears down. With the subprocess seam that boundary is `ctx.subprocess` disposal, so a background process survives an executor-only reload. + +```ts cordis-catalog +/** + * Apply implementation-owned defaults and caps to a request before execution. + * @param request - the caller's request; omitted fields get this + * implementation's defaults, capped fields are clamped. + * @returns the fully-specified spec to hand to {@link run}/{@link start}. + */ +abstract resolve(request: BashExecRequest): BashExecSpec + +/** + * Run a command in the foreground; resolves when it finishes. + * @param spec - a resolved spec from {@link resolve}, never a raw request. + * @returns the outcome; nonzero exits, timeout kills, and abort kills + * resolve with a descriptive result rather than reject. + */ +abstract run(spec: BashExecSpec): Promise + +/** + * Start a background process and return its handle immediately. + * @param spec - a resolved spec from {@link resolve}, never a raw request. + * @returns the live process handle (reads, kill, quiescence promise). + */ +abstract start(spec: BashExecSpec): BashProcess +``` + +Source: [`packages/bash/bash/src/index.ts:53`](../../packages/bash/bash/src/index.ts) + + + +### `ctx.bashEnv` — `BashEnvRegistry` + +Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model shell call: ambient `DSH_*` values are discarded by the executor, then the registry's current snapshot is injected. Built-in shell facts remain owned by the registry itself while plugins can register additional, enumerable facts with effect-scoped disposal. + +```ts cordis-catalog +/** + * Register one environment contributor. Names and keys are unique; built-in + * keys are reserved. Registration is disposed with the calling plugin fiber. + * @param contributor - declared key ownership and per-execution resolver. + * @returns the disposer that unregisters the contribution. + */ +register(contributor: BashEnvContributor): () => void + +/** + * Build the trusted `DSH_*` snapshot for one shell tool execution. + * @param execution - the current tool execution. + * @returns an immutable environment overlay containing built-ins and current contributions. + */ +collect(execution: ToolExecution): DshEnvironment + +/** + * Enumerate plugin-contributed variables without executing their resolvers. + * @returns declarations sorted by environment variable name. + */ +list(): BashEnvVariableInfo[] +``` + +Types: [DshEnvironment](subprocess.md) · [ToolExecution](tools.md) + +Source: [`packages/bash/bash-env/src/index.ts:89`](../../packages/bash/bash-env/src/index.ts) + diff --git a/docs/subsystems/bash.zh.md b/docs/subsystems/bash.zh.md index 27ca9f6e5a..bba9ece295 100644 --- a/docs/subsystems/bash.zh.md +++ b/docs/subsystems/bash.zh.md @@ -219,3 +219,85 @@ interface BashProcessRead { ## 服务 `BashExecutor` 拥有 `resolve`、前台 `run`、后台进程 `start` 以及 `sandboxMode` 能力事实。`dsh-bash-local` 拥有命令默认值补全、超时/中止分类、终端环境以及后台读取合并;进程组、有界收集器、spill 文件、凭据清除与 dispose(资源释放)后完全停稳归[进程管理器](subprocess.md)所有。`dsh-tool-bash` 拥有面向模型的渲染,并将后台句柄适配到[通用任务运行时](tasks.md)。`dsh-bash` 拥有 shell 工具共享的退出状态契约:导出的 `parseExitStatus`/`ParsedExitStatus` 是 `dsh-tool-bash` 的 `renderResult` 与 `dsh-tool-pwsh` 的 `renderPwshResult` 所追加的 `[exit code: N]` / `[killed by signal: X]` 标记的逆解析,两个工具的 `presentResult` 都用它把渲染文本拆分为 terminal 卡的输出正文与退出状态 pill。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.bash` — `BashExecutor` (abstract seam) + +Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). + +Implementations must honor these semantics: + +- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult. +- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr. +- BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files. +- A still-running background process is stopped and awaited when its owning composition tears down. With the subprocess seam that boundary is `ctx.subprocess` disposal, so a background process survives an executor-only reload. + +```ts cordis-catalog +/** + * Apply implementation-owned defaults and caps to a request before execution. + * @param request - the caller's request; omitted fields get this + * implementation's defaults, capped fields are clamped. + * @returns the fully-specified spec to hand to {@link run}/{@link start}. + */ +abstract resolve(request: BashExecRequest): BashExecSpec + +/** + * Run a command in the foreground; resolves when it finishes. + * @param spec - a resolved spec from {@link resolve}, never a raw request. + * @returns the outcome; nonzero exits, timeout kills, and abort kills + * resolve with a descriptive result rather than reject. + */ +abstract run(spec: BashExecSpec): Promise + +/** + * Start a background process and return its handle immediately. + * @param spec - a resolved spec from {@link resolve}, never a raw request. + * @returns the live process handle (reads, kill, quiescence promise). + */ +abstract start(spec: BashExecSpec): BashProcess +``` + +Source: [`packages/bash/bash/src/index.ts:53`](../../packages/bash/bash/src/index.ts) + + + +### `ctx.bashEnv` — `BashEnvRegistry` + +Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model shell call: ambient `DSH_*` values are discarded by the executor, then the registry's current snapshot is injected. Built-in shell facts remain owned by the registry itself while plugins can register additional, enumerable facts with effect-scoped disposal. + +```ts cordis-catalog +/** + * Register one environment contributor. Names and keys are unique; built-in + * keys are reserved. Registration is disposed with the calling plugin fiber. + * @param contributor - declared key ownership and per-execution resolver. + * @returns the disposer that unregisters the contribution. + */ +register(contributor: BashEnvContributor): () => void + +/** + * Build the trusted `DSH_*` snapshot for one shell tool execution. + * @param execution - the current tool execution. + * @returns an immutable environment overlay containing built-ins and current contributions. + */ +collect(execution: ToolExecution): DshEnvironment + +/** + * Enumerate plugin-contributed variables without executing their resolvers. + * @returns declarations sorted by environment variable name. + */ +list(): BashEnvVariableInfo[] +``` + +Types: [DshEnvironment](subprocess.md) · [ToolExecution](tools.md) + +Source: [`packages/bash/bash-env/src/index.ts:89`](../../packages/bash/bash-env/src/index.ts) + diff --git a/docs/subsystems/client-modules.i18n.yaml b/docs/subsystems/client-modules.i18n.yaml index f0df40a866..ae61603169 100644 --- a/docs/subsystems/client-modules.i18n.yaml +++ b/docs/subsystems/client-modules.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/client-modules.md -client-modules.md: 5dc89a87db7e79e4a297002fa059fdeffcceda20 -client-modules.zh.md: b3aa9c2aecf8e051a41b7b05f05d364b9ff8d2a6 +client-modules.md: 19bcc6304e9d917c3950fd9857e7b2c2b3c16e73 +client-modules.zh.md: b0ad293cad2f67de1982ef271308b2a7f06ebaf4 diff --git a/docs/subsystems/client-modules.md b/docs/subsystems/client-modules.md index 5dc89a87db..19bcc6304e 100644 --- a/docs/subsystems/client-modules.md +++ b/docs/subsystems/client-modules.md @@ -58,6 +58,61 @@ Package metadata — including the negative "not a client package" verdict — i ## The service -`ClientModuleHostService` (`ctx.clientModuleHost`, defined in [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts)) exposes reads and the rebuild face; signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxclientmodulehost--clientmodulehostservice). `graph()` returns the current composed graph (a stable object between changes) and `clientPath(id)` the bundle's absolute path. `rebuilt(id)` is the only entry point through which bundle content reaches the graph: it re-hashes the file, and only a real rev change recomposes the graph and notifies. `onRebuilt` fires per changed bundle with the new rev; `onGraphChanged` fires after any flush that recomposed the graph (row added or removed, or a rebuilt rev change) and is pull-model — listeners re-read `graph()`. Both notification paths contain listener exceptions so one throwing subscriber cannot skip later subscribers or kill whatever triggered the flush. +`ClientModuleHostService` (`ctx.clientModuleHost`, defined in [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts)) exposes reads and the rebuild face; signatures are in the generated [service catalog](#ctxclientmodulehost--clientmodulehostservice). `graph()` returns the current composed graph (a stable object between changes) and `clientPath(id)` the bundle's absolute path. `rebuilt(id)` is the only entry point through which bundle content reaches the graph: it re-hashes the file, and only a real rev change recomposes the graph and notifies. `onRebuilt` fires per changed bundle with the new rev; `onGraphChanged` fires after any flush that recomposed the graph (row added or removed, or a rebuilt rev change) and is pull-model — listeners re-read `graph()`. Both notification paths contain listener exceptions so one throwing subscriber cannot skip later subscribers or kill whatever triggered the flush. In development, [dsh-client-hmr](../../packages/client/hmr/README.md) is the registry's watch driver: its node half stat-polls every graph row's bundle from a synchronously captured baseline, calls `rebuilt(id)` on change, resyncs its watch set through `onGraphChanged`, and broadcasts rev changes to the browser half over SSE. Production graphs omit the HMR row entirely; the module host itself never watches files. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.clientModuleHost` — `ClientModuleHostService` + +The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it). + +```ts cordis-catalog +/** + * Current composed entry graph (stable object between changes). + * @returns the graph served as `window.__DSH_BOOT__`. + */ +graph(): WebBootGraph + +/** + * Absolute path of an entry's client bundle. + * @param id - entry id (package name). + * @returns the path, or undefined for an unknown id. + */ +clientPath(id: string): string | undefined + +/** + * Re-hash one bundle (the HMR watch's registration hook — the only entry + * point through which bundle content changes reach the graph). + * @param id - entry id (package name). + * @returns the new rev, or undefined for an unknown id. + */ +rebuilt(id: string): string | undefined + +/** + * Subscribe to bundle rebuilds; fires only when the re-hash changed the rev. + * @param listener - receives the entry id and its new bundle rev. + * @returns the unsubscriber. + */ +onRebuilt(listener: (id: string, rev: string) => void): () => void + +/** + * Fires after any flush that recomposed the graph (row added/removed, or a + * rebuilt rev change). Pull model: listeners re-read {@link graph}. + * @param listener - notified with no payload. + * @returns the unsubscriber. + */ +onGraphChanged(listener: () => void): () => void +``` + +Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts) + diff --git a/docs/subsystems/client-modules.zh.md b/docs/subsystems/client-modules.zh.md index b3aa9c2aec..b0ad293cad 100644 --- a/docs/subsystems/client-modules.zh.md +++ b/docs/subsystems/client-modules.zh.md @@ -58,6 +58,61 @@ interface WebBootGraph { ## 服务 -`ClientModuleHostService`(`ctx.clientModuleHost`,定义于 [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts))暴露读取面与重建面;签名见生成的[服务目录](../cordis-catalog/services.md#ctxclientmodulehost--clientmodulehostservice)。`graph()` 返回当前组合出的图(两次变更之间是同一个稳定对象),`clientPath(id)` 返回该 bundle 的绝对路径。`rebuilt(id)` 是 bundle 内容到达图的唯一入口:它对文件重新哈希,只有 rev 真正变化才会重新组合图并发出通知。`onRebuilt` 按发生变化的 bundle 逐个触发并携带新 rev;`onGraphChanged` 在任何一次重新组合了图的 flush 之后触发(行的增删,或 rebuilt 带来的 rev 变化),并采用拉取模型——监听器自行重读 `graph()`。两条通知路径都会兜住监听器异常,因此一个抛错的订阅者既不能让后续订阅者被跳过,也不能杀死触发这次 flush 的一方。 +`ClientModuleHostService`(`ctx.clientModuleHost`,定义于 [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts))暴露读取面与重建面;签名见生成的[服务目录](#ctxclientmodulehost--clientmodulehostservice)。`graph()` 返回当前组合出的图(两次变更之间是同一个稳定对象),`clientPath(id)` 返回该 bundle 的绝对路径。`rebuilt(id)` 是 bundle 内容到达图的唯一入口:它对文件重新哈希,只有 rev 真正变化才会重新组合图并发出通知。`onRebuilt` 按发生变化的 bundle 逐个触发并携带新 rev;`onGraphChanged` 在任何一次重新组合了图的 flush 之后触发(行的增删,或 rebuilt 带来的 rev 变化),并采用拉取模型——监听器自行重读 `graph()`。两条通知路径都会兜住监听器异常,因此一个抛错的订阅者既不能让后续订阅者被跳过,也不能杀死触发这次 flush 的一方。 开发环境下,[dsh-client-hmr](../../packages/client/hmr/README.md) 是注册表的监视驱动:它的 Node 半从同步取得的基线出发,对图中每一行的 bundle 做 stat 轮询,变化时调用 `rebuilt(id)`,经 `onGraphChanged` 重新同步监视集合,并通过 SSE(Server-Sent Events)把 rev 变化广播给浏览器半。生产环境的图完全不含 HMR(热模块替换)行;模块宿主自身从不监视文件。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.clientModuleHost` — `ClientModuleHostService` + +The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it). + +```ts cordis-catalog +/** + * Current composed entry graph (stable object between changes). + * @returns the graph served as `window.__DSH_BOOT__`. + */ +graph(): WebBootGraph + +/** + * Absolute path of an entry's client bundle. + * @param id - entry id (package name). + * @returns the path, or undefined for an unknown id. + */ +clientPath(id: string): string | undefined + +/** + * Re-hash one bundle (the HMR watch's registration hook — the only entry + * point through which bundle content changes reach the graph). + * @param id - entry id (package name). + * @returns the new rev, or undefined for an unknown id. + */ +rebuilt(id: string): string | undefined + +/** + * Subscribe to bundle rebuilds; fires only when the re-hash changed the rev. + * @param listener - receives the entry id and its new bundle rev. + * @returns the unsubscriber. + */ +onRebuilt(listener: (id: string, rev: string) => void): () => void + +/** + * Fires after any flush that recomposed the graph (row added/removed, or a + * rebuilt rev change). Pull model: listeners re-read {@link graph}. + * @param listener - notified with no payload. + * @returns the unsubscriber. + */ +onGraphChanged(listener: () => void): () => void +``` + +Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts) + diff --git a/docs/subsystems/code-runtime.i18n.yaml b/docs/subsystems/code-runtime.i18n.yaml index 02bbc9587a..05e7e67ed1 100644 --- a/docs/subsystems/code-runtime.i18n.yaml +++ b/docs/subsystems/code-runtime.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/code-runtime.md -code-runtime.md: fbce7d812b7609716fb43ae01610253008e0a92c -code-runtime.zh.md: 700146cfaa9cfab37ec4d85e550020acf1d9f294 +# pnpm run verify-translation-pairing --write docs/subsystems/code-runtime.md +code-runtime.md: 12779f1aafc8930410ae0d695e3f617caafcf607 +code-runtime.zh.md: de563dd3430348f5cf3675d4bf124adeba170559 diff --git a/docs/subsystems/code-runtime.md b/docs/subsystems/code-runtime.md index fbce7d812b..12779f1aaf 100644 --- a/docs/subsystems/code-runtime.md +++ b/docs/subsystems/code-runtime.md @@ -159,3 +159,33 @@ interface CodeRunFailure { ## The service `CodeRuntime` (`ctx.codeRuntime`, abstract — defined in [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)) is `run(request)` plus two readonly descriptors: `language` (what the program must be written in — `'typescript'` and `'python'` are the well-known values, those `dsh-tools` presents, and only `'typescript'` has a published backend; a consumer generating language-specific presentation switches on it and fails loud on one it cannot present) and `isolation` (the execution substrate — `'worker-thread'`, `'process'`, `'container'`; a diagnostic label, **not a security claim**). Implementations must keep runs isolated from each other (no cross-run state) and dispose to quiescence: in-flight runs are terminated and awaited before teardown completes. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.codeRuntime` — `CodeRuntime` (abstract seam) + +Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and substrate failures resolve in CodeRunResult; only seam misuse rejects. Implementations bridge structured-cloneable bindings, materialize each declared namespace rejection class, treat programs as hostile peers, isolate runs from one another, and terminate and await in-flight runs during disposal. + +```ts cordis-catalog +/** + * Execute one program against the request's bindings and capture what it + * emitted. See the class doc for the resolution contract (error is a result + * field; rejection means seam misuse only). + * @param request - the program, its bindings, and the abort signal; the + * request carries everything the runtime acts on, with no hidden defaults. + * @returns the run's outcome: completion value (when transferable), the + * ordered log capture, and the failure (if any). + */ +abstract run(request: CodeRunRequest): Promise +``` + +Source: [`packages/code-runtime/code-runtime/src/index.ts:104`](../../packages/code-runtime/code-runtime/src/index.ts) + diff --git a/docs/subsystems/code-runtime.zh.md b/docs/subsystems/code-runtime.zh.md index 700146cfaa..de563dd343 100644 --- a/docs/subsystems/code-runtime.zh.md +++ b/docs/subsystems/code-runtime.zh.md @@ -159,3 +159,33 @@ interface CodeRunFailure { ## 服务 `CodeRuntime`(`ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))由 `run(request)` 加两个只读描述符组成:`language`(程序必须使用的语言,已知值为 `'typescript'` 与 `'python'`,即 `dsh-tools` 能呈现的那些,其中只有 `'typescript'` 有已发布的后端;生成语言相关展示的消费方据此切换,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底,`'worker-thread'`、`'process'`、`'container'`;仅为诊断标签,**不构成安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),并在 dispose(资源释放)时等待系统完全停稳:teardown 要等到所有进行中的运行均已终止并结算后才完成。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.codeRuntime` — `CodeRuntime` (abstract seam) + +Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and substrate failures resolve in CodeRunResult; only seam misuse rejects. Implementations bridge structured-cloneable bindings, materialize each declared namespace rejection class, treat programs as hostile peers, isolate runs from one another, and terminate and await in-flight runs during disposal. + +```ts cordis-catalog +/** + * Execute one program against the request's bindings and capture what it + * emitted. See the class doc for the resolution contract (error is a result + * field; rejection means seam misuse only). + * @param request - the program, its bindings, and the abort signal; the + * request carries everything the runtime acts on, with no hidden defaults. + * @returns the run's outcome: completion value (when transferable), the + * ordered log capture, and the failure (if any). + */ +abstract run(request: CodeRunRequest): Promise +``` + +Source: [`packages/code-runtime/code-runtime/src/index.ts:104`](../../packages/code-runtime/code-runtime/src/index.ts) + diff --git a/docs/subsystems/commands.i18n.yaml b/docs/subsystems/commands.i18n.yaml index a741971a9a..3bd5420573 100644 --- a/docs/subsystems/commands.i18n.yaml +++ b/docs/subsystems/commands.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/commands.md -commands.md: 83ce9c2498df58110d9a9c2e7d5163a9df813e1c -commands.zh.md: c486f480177c7ff8b35cc846beafac009cea42bb +commands.md: c1ab6d9782f515321104d0cf4d9e037c6a68a18d +commands.zh.md: 3b3d8def042b107c5098e76c511424f023975e77 diff --git a/docs/subsystems/commands.md b/docs/subsystems/commands.md index 83ce9c2498..c1ab6d9782 100644 --- a/docs/subsystems/commands.md +++ b/docs/subsystems/commands.md @@ -97,3 +97,89 @@ interface ParsedCommand { readonly rawInput: string } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.commands` — `CommandService` + +Human-command registry. Plain-context definitions are global; definitions registered through a command-injected child of an agent context shadow globals for that agent. + +```ts cordis-catalog +/** + * Register a global or calling-agent-scoped command. + * @param definition - discovery metadata and direct UI handler. + * @returns the exact effect disposer that unregisters this definition. + */ +register(definition: CommandDefinition): () => void + +/** + * List the effective immutable command descriptors for one agent. + * @param agent - exact receiving agent and scoped-layer key. + * @returns name-sorted descriptors after scoped shadowing. + */ +list(agent: Agent): readonly CommandDescriptor[] + +/** + * Resolve one effective command definition. + * @param agent - exact receiving agent and scoped-layer key. + * @param name - command name without a slash. + * @returns the scoped shadow or global definition. + */ +find(agent: Agent, name: string): CommandDefinition | undefined + +/** + * Parse and execute a known command without sending it to the model. + * + * A resolved command's lifecycle is logged: `command/run` is appended + * before the handler is invoked and `command/done` after settlement (a + * thrown or aborted handler settles as `kind: 'error'`). Both are direct + * log-only appends — no turn wraps them, and persistence drains them at + * ordinary checkpoints. Admission misses (syntax or unknown name) log + * nothing — they never entered a handler. A `command/run` append failure + * fails the execution loud; a `command/done` append failure on the + * handler-failure path is contained so the handler's own error stays the + * reported failure. + * + * @param agent - exact receiving agent. + * @param line - complete slash-command line. + * @param signal - cancellation signal owned by the UI request. + * @returns the settled execution (result + lifecycle pairing id), or + * `undefined` when syntax or name does not resolve. + */ +async execute( agent: Agent, line: string, signal: AbortSignal, ): Promise +``` + +Types: [Agent](core.md) + +Source: [`packages/interaction/commands/src/index.ts:305`](../../packages/interaction/commands/src/index.ts) + + + +### `commands/*` events + + + +#### `commands/change` — emit + +A command was registered or unregistered. This is an unfiltered registry notification because a global or scoped change may affect any UI view. Observer failures are contained and cannot veto the registry mutation. + +```ts cordis-catalog +/** + * A command was registered or unregistered. This is an unfiltered registry + * notification because a global or scoped change may affect any UI view. + * Observer failures are contained and cannot veto the registry mutation. + * @mode emit + */ +'commands/change'(): void +``` + +Source: [`packages/interaction/commands/src/index.ts:172`](../../packages/interaction/commands/src/index.ts) + diff --git a/docs/subsystems/commands.zh.md b/docs/subsystems/commands.zh.md index c486f48017..3b3d8def04 100644 --- a/docs/subsystems/commands.zh.md +++ b/docs/subsystems/commands.zh.md @@ -97,3 +97,89 @@ interface ParsedCommand { readonly rawInput: string } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.commands` — `CommandService` + +Human-command registry. Plain-context definitions are global; definitions registered through a command-injected child of an agent context shadow globals for that agent. + +```ts cordis-catalog +/** + * Register a global or calling-agent-scoped command. + * @param definition - discovery metadata and direct UI handler. + * @returns the exact effect disposer that unregisters this definition. + */ +register(definition: CommandDefinition): () => void + +/** + * List the effective immutable command descriptors for one agent. + * @param agent - exact receiving agent and scoped-layer key. + * @returns name-sorted descriptors after scoped shadowing. + */ +list(agent: Agent): readonly CommandDescriptor[] + +/** + * Resolve one effective command definition. + * @param agent - exact receiving agent and scoped-layer key. + * @param name - command name without a slash. + * @returns the scoped shadow or global definition. + */ +find(agent: Agent, name: string): CommandDefinition | undefined + +/** + * Parse and execute a known command without sending it to the model. + * + * A resolved command's lifecycle is logged: `command/run` is appended + * before the handler is invoked and `command/done` after settlement (a + * thrown or aborted handler settles as `kind: 'error'`). Both are direct + * log-only appends — no turn wraps them, and persistence drains them at + * ordinary checkpoints. Admission misses (syntax or unknown name) log + * nothing — they never entered a handler. A `command/run` append failure + * fails the execution loud; a `command/done` append failure on the + * handler-failure path is contained so the handler's own error stays the + * reported failure. + * + * @param agent - exact receiving agent. + * @param line - complete slash-command line. + * @param signal - cancellation signal owned by the UI request. + * @returns the settled execution (result + lifecycle pairing id), or + * `undefined` when syntax or name does not resolve. + */ +async execute( agent: Agent, line: string, signal: AbortSignal, ): Promise +``` + +Types: [Agent](core.md) + +Source: [`packages/interaction/commands/src/index.ts:305`](../../packages/interaction/commands/src/index.ts) + + + +### `commands/*` events + + + +#### `commands/change` — emit + +A command was registered or unregistered. This is an unfiltered registry notification because a global or scoped change may affect any UI view. Observer failures are contained and cannot veto the registry mutation. + +```ts cordis-catalog +/** + * A command was registered or unregistered. This is an unfiltered registry + * notification because a global or scoped change may affect any UI view. + * Observer failures are contained and cannot veto the registry mutation. + * @mode emit + */ +'commands/change'(): void +``` + +Source: [`packages/interaction/commands/src/index.ts:172`](../../packages/interaction/commands/src/index.ts) + diff --git a/docs/subsystems/compaction.i18n.yaml b/docs/subsystems/compaction.i18n.yaml index 7dc00cebfc..5481b2feb1 100644 --- a/docs/subsystems/compaction.i18n.yaml +++ b/docs/subsystems/compaction.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/compaction.md -compaction.md: f1df5b83bd43136af60988dabd9dc68fe32467a2 -compaction.zh.md: 52540250d466f81f51cf7c681bb6f1436e29a12d +compaction.md: 126c6c7c7df125cf991ea074e0165481c09455dc +compaction.zh.md: 9fb01a41a92367b9bb3f4586f35b5d0baf613bbc diff --git a/docs/subsystems/compaction.md b/docs/subsystems/compaction.md index f1df5b83bd..126c6c7c7d 100644 --- a/docs/subsystems/compaction.md +++ b/docs/subsystems/compaction.md @@ -112,3 +112,119 @@ interface PruneResult { readonly charsRemoved: number } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.compact` — `CompactService` (abstract seam) + +Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses COMPACT_CHECKPOINT_SOURCE so consumers recognize it independently of the backend. Load one implementation per context as `ctx.compact`. + +```ts cordis-catalog +/** + * Consider automatic compaction for one explicit trigger. Pressure policy + * uses the latest durable routed request, while context-overflow policy may + * force a useful balanced reduction even below the normal threshold. Return + * `null` when no safe range can be compacted. A single oversized retained + * unit or request envelope cannot be repaired through surface compaction. + * + * @param agent - agent context owning the session surface and routing options. + * @param trigger - normal pressure or provider-confirmed context overflow. + * @param signal - cancellation signal; model-backed implementations must forward it. + * @returns the compaction result, or `null` if no compaction was needed. + */ +abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger, signal: AbortSignal, ): Promise + +/** + * Explicitly compact useful history even below automatic pressure thresholds. + * Implementations synchronously start an idle task before any asynchronous + * work, select a useful range without writing on a no-op, then + * append a standalone `compact/start` before summarization. That durable + * marker is the compaction lock until one `compact/end` attempt. Later waking + * prompts remain accepted in FIFO order and start only after the optional + * durability checkpoint and idle-task settlement. Context injected while the + * summary runs may sit between the marker pair; only the selected span must + * remain stable. + * + * @param agent - idle agent whose durable history should be compacted. + * @param signal - cancellation scoped to this compaction request. + * @returns the compaction result, or `null` when no safe useful range exists. + * @throws {@link ManualCompactionError} for expected busy, agent-cancellation, + * changed-span, summarization/shrink, commit-stage, or persistence failures; + * an aborted request preserves its exact abort reason. Failed attempts remain + * visible in the log. + */ +abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): Promise + +/** + * Forcibly compact a range of surface nodes into a single summary node. + * `start` and `end` name an inclusive span by surface position, not numeric seq + * order; replacements can make visible seqs non-monotonic. Both edges must be + * balanced so assistant tool calls remain paired with their results. A model- + * backed implementation forwards cancellation and rejects active, missing, + * reversed, or unbalanced ranges. The target session is `agent.session`. + * Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}. + * Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter} + * for the edge checks. + * + * @param start - first surface seq, inclusive. + * @param end - last surface seq, inclusive. + * @param agent - context whose session is mutated and whose routing options guide summarization. + * @param signal - optional cancellation; model-backed implementations must forward it. + * @throws when compaction is active or the range is missing, reversed, or unbalanced. + * @returns the appended event seqs, summary, replaced range, and token accounting. + */ +abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise +``` + +Source: [`packages/compact/compact/src/index.ts:93`](../../packages/compact/compact/src/index.ts) + + + +### `ctx.toolResultPrune` — `ToolResultPruneService` + +Deterministic head/middle/tail pruning for current tool-result surface nodes. + +```ts cordis-catalog +/** + * Measure text content in Unicode code points; non-text blocks cost zero. + * @param blocks - tool-result content to measure. + * @returns total Unicode code points across text blocks. + */ +measureContent(blocks: readonly ContentBlock[]): number + +/** + * Replace an over-budget text middle while retaining rich-block order. + * Text slicing is by Unicode code point, not UTF-16 code unit, so a retained + * boundary cannot split a surrogate pair. Grapheme clusters may still split. + * @param blocks - original tool-result content. + * @returns pruned content, or `null` when the text is within budget. + */ +pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null + +/** + * Prune every over-budget tool result from one stable current-surface snapshot. + * Each replacement preserves the complete event data except for `content`, + * points at the shadowed node for durable provenance and replay, and is + * immediately preceded by a `compact/prune` shadow-price event pricing the + * shadowed node through the injected token meter, so pure consumers can + * subtract it without per-node state. + * @param session - session whose current surface is rewritten. + * @returns landed replacements and aggregate Unicode-code-point savings. + * @throws when the session rejects a replacement; replacements committed + * earlier in the pass remain durable. + */ +pruneSession(session: Session): PruneResult +``` + +Types: [ContentBlock](core.md) · [Session](session.md) + +Source: [`packages/compact/compact-tool-result-prune/src/index.ts:44`](../../packages/compact/compact-tool-result-prune/src/index.ts) + diff --git a/docs/subsystems/compaction.zh.md b/docs/subsystems/compaction.zh.md index 52540250d4..9fb01a41a9 100644 --- a/docs/subsystems/compaction.zh.md +++ b/docs/subsystems/compaction.zh.md @@ -112,3 +112,119 @@ interface PruneResult { readonly charsRemoved: number } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.compact` — `CompactService` (abstract seam) + +Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses COMPACT_CHECKPOINT_SOURCE so consumers recognize it independently of the backend. Load one implementation per context as `ctx.compact`. + +```ts cordis-catalog +/** + * Consider automatic compaction for one explicit trigger. Pressure policy + * uses the latest durable routed request, while context-overflow policy may + * force a useful balanced reduction even below the normal threshold. Return + * `null` when no safe range can be compacted. A single oversized retained + * unit or request envelope cannot be repaired through surface compaction. + * + * @param agent - agent context owning the session surface and routing options. + * @param trigger - normal pressure or provider-confirmed context overflow. + * @param signal - cancellation signal; model-backed implementations must forward it. + * @returns the compaction result, or `null` if no compaction was needed. + */ +abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger, signal: AbortSignal, ): Promise + +/** + * Explicitly compact useful history even below automatic pressure thresholds. + * Implementations synchronously start an idle task before any asynchronous + * work, select a useful range without writing on a no-op, then + * append a standalone `compact/start` before summarization. That durable + * marker is the compaction lock until one `compact/end` attempt. Later waking + * prompts remain accepted in FIFO order and start only after the optional + * durability checkpoint and idle-task settlement. Context injected while the + * summary runs may sit between the marker pair; only the selected span must + * remain stable. + * + * @param agent - idle agent whose durable history should be compacted. + * @param signal - cancellation scoped to this compaction request. + * @returns the compaction result, or `null` when no safe useful range exists. + * @throws {@link ManualCompactionError} for expected busy, agent-cancellation, + * changed-span, summarization/shrink, commit-stage, or persistence failures; + * an aborted request preserves its exact abort reason. Failed attempts remain + * visible in the log. + */ +abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): Promise + +/** + * Forcibly compact a range of surface nodes into a single summary node. + * `start` and `end` name an inclusive span by surface position, not numeric seq + * order; replacements can make visible seqs non-monotonic. Both edges must be + * balanced so assistant tool calls remain paired with their results. A model- + * backed implementation forwards cancellation and rejects active, missing, + * reversed, or unbalanced ranges. The target session is `agent.session`. + * Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}. + * Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter} + * for the edge checks. + * + * @param start - first surface seq, inclusive. + * @param end - last surface seq, inclusive. + * @param agent - context whose session is mutated and whose routing options guide summarization. + * @param signal - optional cancellation; model-backed implementations must forward it. + * @throws when compaction is active or the range is missing, reversed, or unbalanced. + * @returns the appended event seqs, summary, replaced range, and token accounting. + */ +abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise +``` + +Source: [`packages/compact/compact/src/index.ts:93`](../../packages/compact/compact/src/index.ts) + + + +### `ctx.toolResultPrune` — `ToolResultPruneService` + +Deterministic head/middle/tail pruning for current tool-result surface nodes. + +```ts cordis-catalog +/** + * Measure text content in Unicode code points; non-text blocks cost zero. + * @param blocks - tool-result content to measure. + * @returns total Unicode code points across text blocks. + */ +measureContent(blocks: readonly ContentBlock[]): number + +/** + * Replace an over-budget text middle while retaining rich-block order. + * Text slicing is by Unicode code point, not UTF-16 code unit, so a retained + * boundary cannot split a surrogate pair. Grapheme clusters may still split. + * @param blocks - original tool-result content. + * @returns pruned content, or `null` when the text is within budget. + */ +pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null + +/** + * Prune every over-budget tool result from one stable current-surface snapshot. + * Each replacement preserves the complete event data except for `content`, + * points at the shadowed node for durable provenance and replay, and is + * immediately preceded by a `compact/prune` shadow-price event pricing the + * shadowed node through the injected token meter, so pure consumers can + * subtract it without per-node state. + * @param session - session whose current surface is rewritten. + * @returns landed replacements and aggregate Unicode-code-point savings. + * @throws when the session rejects a replacement; replacements committed + * earlier in the pass remain durable. + */ +pruneSession(session: Session): PruneResult +``` + +Types: [ContentBlock](core.md) · [Session](session.md) + +Source: [`packages/compact/compact-tool-result-prune/src/index.ts:44`](../../packages/compact/compact-tool-result-prune/src/index.ts) + diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index a2af261bc6..48d6fc9c71 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/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: 3a8ccbef4163eabba1cd287284eda68135f2db13 -core.zh.md: 0cd6a57971ee98c75b52dd45b6f648632dab8a90 +core.md: 7e62de88519e5c0bf3a1a0b94de93635767f9f7b +core.zh.md: ef5b70ae336d55fd25c2251372705912473d07f7 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 3a8ccbef41..7e62de8851 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -53,9 +53,10 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t | [storage.md](storage.md) | the storage subsystem: the backend seam (`StorageBackend`), `StorageForms`, `DomainSpec`/`Domain`, `domain/changed` | | [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 | | [telemetry.md](telemetry.md) | the outbound reporting seam: `TelemetryRecord`/`TelemetrySeverity`, the `TelemetryBackend` contract, the `telemetry/record` redact waterfall | -> Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services use the generated [service catalog](../cordis-catalog/services.md). +> Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services use the generated [service catalog](#cordis-surface). ## The `…Map → derived-union` pattern @@ -780,3 +781,546 @@ type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact' 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)**. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.agentLoop` — `AgentLoop` + +Concrete agent factory and driver service. + +```ts cordis-catalog +/** + * Create an agent and session under one caller-supplied identity, owned by + * the accessing fiber. Constructor-driven config calls mint a fresh combined + * id before entering this boundary. + * @param id - shared agent/session identity. + * @param options - concrete loop options. + * @param meta - optional fresh-session workspace metadata. + * @returns the published running agent. + */ +create(id: SessionId, options: AgentOptions = {}, meta: Pick = {}): Agent + +/** + * Create an owned agent on a caller-supplied session id. + * @param ownerCtx - caller context that structurally owns the lifecycle. + * @param options - identities, session seed/metadata, loop options, setup, and cancellation. + * @returns the published handle. + */ +async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise + +/** + * Resume an owned agent from the configured persistence service. + * @param ownerCtx - caller context that owns load, setup, and the live lifecycle. + * @param options - persisted identity, loop options, setup, and cancellation. + * @returns the published handle. + */ +async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise +``` + +Types: [SessionHeader](persistence.md) + +Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts) + + + +### `ctx.agents` — `AgentRegistry` + +Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory. + +Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain. + +```ts cordis-catalog +/** + * Read the Agent that initiated the inherited asynchronous driver chain. + * Use this optional form for logging, tracing, metrics, or host attribution + * that also supports agentless calls. When a parent creates a child, setup + * reports the causal parent while `agentCtx.agent` identifies the child. + * @returns the inherited Agent, or `undefined` outside an initiator boundary + * and inside an explicit clearing boundary. + * @throws when this service instance has been disposed. + */ +currentInitiator(): Agent | undefined + +/** + * Read the initiating Agent and fail when no initiator boundary is active. + * Use this for private helpers contractually below a driver, or for a + * deployment-owned outbound request whose contract forbids agentless calls. + * Generic or direct-call seams use optional lookup or explicit request fields. + * @returns the inherited Agent. + * @throws when no initiator is active or this service instance has been disposed. + */ +requireInitiator(): Agent + +/** + * Run an operation with one exact Agent as its process-local initiator. The + * exact synchronous value or Promise returned by the operation is preserved. + * Custom drivers and test harnesses wrap their complete returned foreground + * lifetime. + * A queue or wire receiver may establish this boundary only after validating + * explicit identity and resolving the exact live Agent; this method does neither. + * Detached work remains owned by the subsystem that starts it. + * @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization. + * @param operation - synchronous or asynchronous operation to invoke. + * @returns the exact value returned by `operation`. + * @throws when the initiator scope is closing/disposed, or when `operation` throws. + */ +withInitiator(agent: Agent, operation: () => T): T + +/** + * Run an operation inside a boundary that hides any inherited initiating + * Agent. The exact synchronous value or Promise is preserved. + * Use this while creating lazy shared timers, queue pumps, pool maintenance, + * watchers, or exporters so they do not inherit the first Agent that happens + * to initialize them. It clears only initiator attribution, not explicit + * fields, and does not own or drain detached resources. + * @param operation - synchronous or asynchronous operation to invoke without an initiator. + * @returns the exact value returned by `operation`. + * @throws when the initiator scope is closing/disposed, or when `operation` throws. + */ +withoutInitiator(operation: () => T): T + +/** + * Register the agent-creation factory (the loop calls this on construction, + * effect-scoped). A traced Cordis service is canonicalized to its concrete + * target; each create/resume call is then traced through that caller's + * context so ownership follows the caller without stacking proxy layers. + * Throws if a factory is already registered. Returns the disposer; on + * dispose the factory slot is cleared. + * @param factory - the loop-owned factory {@link create}/{@link resume} delegate to. + * @returns the disposer that clears the factory slot. The exact + * Cordis effect disposer (single-shot): composite (generator) effects may + * yield it directly — exact identity nests the teardown in order. + */ +setFactory(factory: AgentFactory): () => void + +/** + * Create and publish a new agent through the registered factory. + * Distinct from {@link register} (which records an already-constructed + * agent): this constructs the agent and its session. Rejects if no factory is + * registered or creation/setup fails. The resolved {@link AgentHandle} lets + * the owner tear down exactly this agent. + * @param options - shared identity, session seed/metadata, and agent options. + * @returns the handle after setup, rollback-covered publication, and loop start complete. + */ +async create(options: CreateAgentOptions): Promise + +/** + * Load a persisted session and resume an agent on it through the registered + * factory. Rejects if no factory is registered; the factory rejects if + * session persistence is not configured or persistence/setup fails. + * @param options - persisted identity, configuration, and optional setup. + * @returns the handle after setup, rollback-covered publication, and loop start complete. + */ +async resume(options: ResumeAgentOptions): Promise + +/** + * Register a live agent. Throws if an agent with the same id is already + * registered. Emits `agent/created` on registration and `agent/disposed` + * when the calling fiber is disposed — both with the agent's scope carrier + * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the + * emits are scope-filtered regardless of which context invoked `register` + * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always + * requires passing the carrier). Returns the disposer. + * @param agent - the already-constructed agent to record in the store. + * @returns the EXACT Cordis effect disposer (single-shot; a repeat call + * returns undefined without awaiting an in-flight teardown). Exact + * identity is load-bearing: a composite (generator) effect that owns a + * teardown ORDER — the agent factory's lifecycle chain — must yield THIS + * function so Cordis nests the unregistration at that yield position; + * yielding a wrapper would leave it disposing as a concurrent sibling on + * owner unload, unregistering the agent (and emitting `agent/disposed`) + * while its final turn is still draining. + */ +register(agent: Agent): () => void + +/** + * Insert an already-constructed agent without announcing it. This is the + * advanced ordered-lifecycle primitive used by the async agent factory: it + * first completes setup while the agent is unpublished, then assigns the + * returned detach closure into its pre-installed composite teardown before + * calling {@link announce}. Ordinary callers use {@link register}. + * @param agent - the prepared, unpublished agent. + * @param owner - live agent whose scoped context created this agent, or + * undefined for a top-level runtime root. This is runtime ownership, not + * the resumed session's durable parent lineage. + * @returns an idempotent closure that removes this exact entry and emits + * `agent/disposed` with listener failures contained. When called from a + * synchronous `agent/created` listener, removal and disposal wait until + * that creation dispatch unwinds. + */ +enter(agent: Agent, owner: Agent | undefined): () => void + +/** + * Announce an agent previously inserted with {@link enter}. + * @param agent - the live inserted agent to announce. + * @throws if `agent` is not the exact live registry entry for its id, or its + * creation announcement already began (including a reentrant call from a + * creation listener). + */ +announce(agent: Agent): void + +/** + * Look up a live agent. + * @param id - the shared agent/session id to look up. + * @returns the agent, or undefined when no live agent has that id. + */ +get(id: SessionId): Agent | undefined + +/** + * Test whether a live agent was created through one exact parent agent's + * scoped context. Runtime ownership is independent of durable session + * lineage and remains unambiguous when unrelated providers reuse an id. + * @param id - the candidate child agent's shared agent/session id. + * @param owner - the expected runtime creator agent. + * @returns true only while the exact child entry is live under that owner. + */ +isOwnedBy(id: SessionId, owner: Agent): boolean + +/** + * All live agents, in registration order. + * @returns a fresh array; mutating it does not affect the registry. + */ +list(): Agent[] + +/** + * All live top-level agents in registration order. A top-level agent was + * created without an owning agent context; durable session lineage does not + * affect this runtime relation, so a resumed fork may still be a root. + * @returns a fresh array; mutating it does not affect the registry. + */ +roots(): Agent[] +``` + +Source: [`packages/core/agent/src/index.ts:253`](../../packages/core/agent/src/index.ts) + + + +### `agent/*` events + + + +#### `agent/created` — emit + +A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving seam. 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. + +```ts cordis-catalog +/** + * A fully configured agent and live session were published. Setup is + * composition-only; `agent/session-start` is the first startup-driving seam. + * 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 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, payload: { agent: Agent }): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:158`](../../packages/core/agent/src/types.ts) + + + +#### `agent/disposed` — emit + +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. + +```ts cordis-catalog +/** + * 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 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, payload: { agent: Agent }): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/types.ts) + + + +#### `agent/error` — emit + +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 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, 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) + + + +#### `agent/inbox/claimed` — emit + +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 +/** + * 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/claimed'(this: Scoped, payload: { agent: Agent; message: UserMessage; turn: number }): void +``` + +Types: [Scoped](scope.md) · [UserMessage](session.md) + +Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts) + + + +#### `agent/inbox/discarded` — emit + +One message was discarded from the live inbox. + +```ts cordis-catalog +/** + * 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/discarded'(this: Scoped, payload: { agent: Agent; message: UserMessage }): void +``` + +Types: [Scoped](scope.md) · [UserMessage](session.md) + +Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts) + + + +#### `agent/inbox/inserted` — emit + +One message entered the live inbox. + +```ts cordis-catalog +/** + * 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/inserted'(this: Scoped, payload: { agent: Agent; message: UserMessage }): void +``` + +Types: [Scoped](scope.md) · [UserMessage](session.md) + +Source: [`packages/core/agent/src/types.ts:185`](../../packages/core/agent/src/types.ts) + + + +#### `agent/pre-step` — waterfall + +Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages. + +```ts cordis-catalog +/** + * 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/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) · [UserMessage](session.md) + +Source: [`packages/core/agent/src/types.ts:230`](../../packages/core/agent/src/types.ts) + + + +#### `agent/request` — waterfall + +Replace the frozen call configuration. `await next()` yields the config 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. + +```ts cordis-catalog +/** + * Replace the frozen call configuration. `await next()` yields the config + * 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 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, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts) + + + +#### `agent/request-error` — waterfall + +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 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, payload: { agent: Agent; turn: number; step: number; provider: string; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined; signal: AbortSignal }, next: () => Promise): Promise +``` + +Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md) · [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts) + + + +#### `agent/session-start` — emit + +The session lifecycle began, once before the first turn. Use `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. + +```ts cordis-catalog +/** + * The session lifecycle began, once before the first turn. Use + * `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 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, payload: { agent: Agent; source: SessionStartSource }): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:216`](../../packages/core/agent/src/types.ts) + + + +#### `agent/status` — emit + +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`). 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, payload: { agent: Agent; status: AgentStatus }): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:177`](../../packages/core/agent/src/types.ts) + + + +#### `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 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 +/** + * 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. + * @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, payload: { agent: Agent; turn: number; signal: AbortSignal }): Promise | void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:277`](../../packages/core/agent/src/types.ts) + + + +### `agent-loop/*` events + + + +#### `agent-loop/config-start-failed` — emit + +A declarative agent entry failed before it could publish a live agent. 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. + +```ts cordis-catalog +/** + * A declarative agent entry failed before it could publish a live agent. + * 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 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'(payload: { sessionId: SessionId; error: unknown }): void +``` + +Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/src/index.ts) + diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 0cd6a57971..ef5b70ae33 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -53,9 +53,10 @@ harness 是一个微内核:一个极小的核心加上众多插件。大多数 | [storage.md](storage.md) | 存储子系统:后端 seam(`StorageBackend`)、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed` | | [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` 的一致切面、变更馈送 | | [telemetry.md](telemetry.md) | 对外上报 seam:`TelemetryRecord`/`TelemetrySeverity`、`TelemetryBackend` 契约、`telemetry/record` 脱敏 waterfall | -> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务使用生成的[服务目录](../cordis-catalog/services.md)。 +> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务使用生成的[服务目录](#cordis-surface)。 @@ -788,3 +789,546 @@ type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact' 唯一属于核心的流水线编写类型:每个已注册工具*是什么*——一个面向模型的 `ToolSchema` 加上一个 `execute` 函数,以及可选的最终内容回调与 UI 回调。工具作者很少手动构造它(`defineTool` DSL 会用类型化参数构建),但它是注册表持有、循环分发所经过的契约。 其完整字段、`defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` 类型化 schema DSL、`ToolExecution`/`ToolExecutionResult` waterfall 形状,以及工具展示 UI 词汇在 **[tools.md](tools.md)** 中。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.agentLoop` — `AgentLoop` + +Concrete agent factory and driver service. + +```ts cordis-catalog +/** + * Create an agent and session under one caller-supplied identity, owned by + * the accessing fiber. Constructor-driven config calls mint a fresh combined + * id before entering this boundary. + * @param id - shared agent/session identity. + * @param options - concrete loop options. + * @param meta - optional fresh-session workspace metadata. + * @returns the published running agent. + */ +create(id: SessionId, options: AgentOptions = {}, meta: Pick = {}): Agent + +/** + * Create an owned agent on a caller-supplied session id. + * @param ownerCtx - caller context that structurally owns the lifecycle. + * @param options - identities, session seed/metadata, loop options, setup, and cancellation. + * @returns the published handle. + */ +async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise + +/** + * Resume an owned agent from the configured persistence service. + * @param ownerCtx - caller context that owns load, setup, and the live lifecycle. + * @param options - persisted identity, loop options, setup, and cancellation. + * @returns the published handle. + */ +async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise +``` + +Types: [SessionHeader](persistence.md) + +Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts) + + + +### `ctx.agents` — `AgentRegistry` + +Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory. + +Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain. + +```ts cordis-catalog +/** + * Read the Agent that initiated the inherited asynchronous driver chain. + * Use this optional form for logging, tracing, metrics, or host attribution + * that also supports agentless calls. When a parent creates a child, setup + * reports the causal parent while `agentCtx.agent` identifies the child. + * @returns the inherited Agent, or `undefined` outside an initiator boundary + * and inside an explicit clearing boundary. + * @throws when this service instance has been disposed. + */ +currentInitiator(): Agent | undefined + +/** + * Read the initiating Agent and fail when no initiator boundary is active. + * Use this for private helpers contractually below a driver, or for a + * deployment-owned outbound request whose contract forbids agentless calls. + * Generic or direct-call seams use optional lookup or explicit request fields. + * @returns the inherited Agent. + * @throws when no initiator is active or this service instance has been disposed. + */ +requireInitiator(): Agent + +/** + * Run an operation with one exact Agent as its process-local initiator. The + * exact synchronous value or Promise returned by the operation is preserved. + * Custom drivers and test harnesses wrap their complete returned foreground + * lifetime. + * A queue or wire receiver may establish this boundary only after validating + * explicit identity and resolving the exact live Agent; this method does neither. + * Detached work remains owned by the subsystem that starts it. + * @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization. + * @param operation - synchronous or asynchronous operation to invoke. + * @returns the exact value returned by `operation`. + * @throws when the initiator scope is closing/disposed, or when `operation` throws. + */ +withInitiator(agent: Agent, operation: () => T): T + +/** + * Run an operation inside a boundary that hides any inherited initiating + * Agent. The exact synchronous value or Promise is preserved. + * Use this while creating lazy shared timers, queue pumps, pool maintenance, + * watchers, or exporters so they do not inherit the first Agent that happens + * to initialize them. It clears only initiator attribution, not explicit + * fields, and does not own or drain detached resources. + * @param operation - synchronous or asynchronous operation to invoke without an initiator. + * @returns the exact value returned by `operation`. + * @throws when the initiator scope is closing/disposed, or when `operation` throws. + */ +withoutInitiator(operation: () => T): T + +/** + * Register the agent-creation factory (the loop calls this on construction, + * effect-scoped). A traced Cordis service is canonicalized to its concrete + * target; each create/resume call is then traced through that caller's + * context so ownership follows the caller without stacking proxy layers. + * Throws if a factory is already registered. Returns the disposer; on + * dispose the factory slot is cleared. + * @param factory - the loop-owned factory {@link create}/{@link resume} delegate to. + * @returns the disposer that clears the factory slot. The exact + * Cordis effect disposer (single-shot): composite (generator) effects may + * yield it directly — exact identity nests the teardown in order. + */ +setFactory(factory: AgentFactory): () => void + +/** + * Create and publish a new agent through the registered factory. + * Distinct from {@link register} (which records an already-constructed + * agent): this constructs the agent and its session. Rejects if no factory is + * registered or creation/setup fails. The resolved {@link AgentHandle} lets + * the owner tear down exactly this agent. + * @param options - shared identity, session seed/metadata, and agent options. + * @returns the handle after setup, rollback-covered publication, and loop start complete. + */ +async create(options: CreateAgentOptions): Promise + +/** + * Load a persisted session and resume an agent on it through the registered + * factory. Rejects if no factory is registered; the factory rejects if + * session persistence is not configured or persistence/setup fails. + * @param options - persisted identity, configuration, and optional setup. + * @returns the handle after setup, rollback-covered publication, and loop start complete. + */ +async resume(options: ResumeAgentOptions): Promise + +/** + * Register a live agent. Throws if an agent with the same id is already + * registered. Emits `agent/created` on registration and `agent/disposed` + * when the calling fiber is disposed — both with the agent's scope carrier + * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the + * emits are scope-filtered regardless of which context invoked `register` + * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always + * requires passing the carrier). Returns the disposer. + * @param agent - the already-constructed agent to record in the store. + * @returns the EXACT Cordis effect disposer (single-shot; a repeat call + * returns undefined without awaiting an in-flight teardown). Exact + * identity is load-bearing: a composite (generator) effect that owns a + * teardown ORDER — the agent factory's lifecycle chain — must yield THIS + * function so Cordis nests the unregistration at that yield position; + * yielding a wrapper would leave it disposing as a concurrent sibling on + * owner unload, unregistering the agent (and emitting `agent/disposed`) + * while its final turn is still draining. + */ +register(agent: Agent): () => void + +/** + * Insert an already-constructed agent without announcing it. This is the + * advanced ordered-lifecycle primitive used by the async agent factory: it + * first completes setup while the agent is unpublished, then assigns the + * returned detach closure into its pre-installed composite teardown before + * calling {@link announce}. Ordinary callers use {@link register}. + * @param agent - the prepared, unpublished agent. + * @param owner - live agent whose scoped context created this agent, or + * undefined for a top-level runtime root. This is runtime ownership, not + * the resumed session's durable parent lineage. + * @returns an idempotent closure that removes this exact entry and emits + * `agent/disposed` with listener failures contained. When called from a + * synchronous `agent/created` listener, removal and disposal wait until + * that creation dispatch unwinds. + */ +enter(agent: Agent, owner: Agent | undefined): () => void + +/** + * Announce an agent previously inserted with {@link enter}. + * @param agent - the live inserted agent to announce. + * @throws if `agent` is not the exact live registry entry for its id, or its + * creation announcement already began (including a reentrant call from a + * creation listener). + */ +announce(agent: Agent): void + +/** + * Look up a live agent. + * @param id - the shared agent/session id to look up. + * @returns the agent, or undefined when no live agent has that id. + */ +get(id: SessionId): Agent | undefined + +/** + * Test whether a live agent was created through one exact parent agent's + * scoped context. Runtime ownership is independent of durable session + * lineage and remains unambiguous when unrelated providers reuse an id. + * @param id - the candidate child agent's shared agent/session id. + * @param owner - the expected runtime creator agent. + * @returns true only while the exact child entry is live under that owner. + */ +isOwnedBy(id: SessionId, owner: Agent): boolean + +/** + * All live agents, in registration order. + * @returns a fresh array; mutating it does not affect the registry. + */ +list(): Agent[] + +/** + * All live top-level agents in registration order. A top-level agent was + * created without an owning agent context; durable session lineage does not + * affect this runtime relation, so a resumed fork may still be a root. + * @returns a fresh array; mutating it does not affect the registry. + */ +roots(): Agent[] +``` + +Source: [`packages/core/agent/src/index.ts:253`](../../packages/core/agent/src/index.ts) + + + +### `agent/*` events + + + +#### `agent/created` — emit + +A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving seam. 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. + +```ts cordis-catalog +/** + * A fully configured agent and live session were published. Setup is + * composition-only; `agent/session-start` is the first startup-driving seam. + * 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 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, payload: { agent: Agent }): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:158`](../../packages/core/agent/src/types.ts) + + + +#### `agent/disposed` — emit + +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. + +```ts cordis-catalog +/** + * 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 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, payload: { agent: Agent }): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/types.ts) + + + +#### `agent/error` — emit + +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 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, 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) + + + +#### `agent/inbox/claimed` — emit + +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 +/** + * 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/claimed'(this: Scoped, payload: { agent: Agent; message: UserMessage; turn: number }): void +``` + +Types: [Scoped](scope.md) · [UserMessage](session.md) + +Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts) + + + +#### `agent/inbox/discarded` — emit + +One message was discarded from the live inbox. + +```ts cordis-catalog +/** + * 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/discarded'(this: Scoped, payload: { agent: Agent; message: UserMessage }): void +``` + +Types: [Scoped](scope.md) · [UserMessage](session.md) + +Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts) + + + +#### `agent/inbox/inserted` — emit + +One message entered the live inbox. + +```ts cordis-catalog +/** + * 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/inserted'(this: Scoped, payload: { agent: Agent; message: UserMessage }): void +``` + +Types: [Scoped](scope.md) · [UserMessage](session.md) + +Source: [`packages/core/agent/src/types.ts:185`](../../packages/core/agent/src/types.ts) + + + +#### `agent/pre-step` — waterfall + +Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages. + +```ts cordis-catalog +/** + * 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/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) · [UserMessage](session.md) + +Source: [`packages/core/agent/src/types.ts:230`](../../packages/core/agent/src/types.ts) + + + +#### `agent/request` — waterfall + +Replace the frozen call configuration. `await next()` yields the config 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. + +```ts cordis-catalog +/** + * Replace the frozen call configuration. `await next()` yields the config + * 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 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, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts) + + + +#### `agent/request-error` — waterfall + +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 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, payload: { agent: Agent; turn: number; step: number; provider: string; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined; signal: AbortSignal }, next: () => Promise): Promise +``` + +Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md) · [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts) + + + +#### `agent/session-start` — emit + +The session lifecycle began, once before the first turn. Use `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. + +```ts cordis-catalog +/** + * The session lifecycle began, once before the first turn. Use + * `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 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, payload: { agent: Agent; source: SessionStartSource }): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:216`](../../packages/core/agent/src/types.ts) + + + +#### `agent/status` — emit + +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`). 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, payload: { agent: Agent; status: AgentStatus }): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:177`](../../packages/core/agent/src/types.ts) + + + +#### `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 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 +/** + * 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. + * @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, payload: { agent: Agent; turn: number; signal: AbortSignal }): Promise | void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/types.ts:277`](../../packages/core/agent/src/types.ts) + + + +### `agent-loop/*` events + + + +#### `agent-loop/config-start-failed` — emit + +A declarative agent entry failed before it could publish a live agent. 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. + +```ts cordis-catalog +/** + * A declarative agent entry failed before it could publish a live agent. + * 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 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'(payload: { sessionId: SessionId; error: unknown }): void +``` + +Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/src/index.ts) + diff --git a/docs/subsystems/credentials.i18n.yaml b/docs/subsystems/credentials.i18n.yaml index f7899eaf6e..7028d71f8d 100644 --- a/docs/subsystems/credentials.i18n.yaml +++ b/docs/subsystems/credentials.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/credentials.md -credentials.md: ef74ddeb4346e18f8d5d33488657e5d50f1d754e -credentials.zh.md: 09cf374a2346fd93aa834e3372321e6eeece6ed8 +credentials.md: 0bc2224ac039addc795d3806e8c85004f2bb84a7 +credentials.zh.md: 6f5ccd1572885434257d39eb0f659fcc7371ad1d diff --git a/docs/subsystems/credentials.md b/docs/subsystems/credentials.md index ef74ddeb43..0bc2224ac0 100644 --- a/docs/subsystems/credentials.md +++ b/docs/subsystems/credentials.md @@ -48,3 +48,86 @@ interface CredentialInfo { ## Change commits `credentials/updated (ref)` fires after a committed change to a provider-managed source — a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Consumers do not need the event (they re-resolve per operation); it exists for configuration surfaces refreshing a "configured" badge. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.credentials` — `Credentials` (abstract seam) + +Abstract credential service. Providers implement the four operations over their source layers; one seam-wide rule binds them all: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret. + +```ts cordis-catalog +/** + * Resolve one reference to its current value. Resolution is per call: + * consumers re-resolve at each operation and must not cache across + * operations — that per-operation read is what makes a changed credential + * reach the next operation without a restart. + * @param ref - the reference to resolve. + * @returns the value and its source, or `undefined` while unconfigured. + */ +abstract resolve(ref: CredentialRef): Promise + +/** + * Describe one reference for configuration surfaces without exposing the + * value. + * @param ref - the reference to describe. + * @returns configured state, supplying source, and writability. + */ +abstract describe(ref: CredentialRef): Promise + +/** + * Durably store one value in the provider-managed writable source. Rejects + * while a read-only source shadows the reference — the write would appear + * to succeed while resolution keeps returning the shadowing value — and + * rejects an empty value (use {@link unset}). + * @param ref - the reference to store. + * @param value - the non-empty secret value. + */ +abstract set(ref: CredentialRef, value: string): Promise + +/** + * Remove one reference from the provider-managed writable source; removing + * an absent reference is a no-op. Rejects while a read-only source shadows + * the reference, like {@link set}. + * @param ref - the reference to remove. + */ +abstract unset(ref: CredentialRef): Promise +``` + +Source: [`packages/credentials/credentials/src/index.ts:77`](../../packages/credentials/credentials/src/index.ts) + + + +### `credentials/*` events + + + +#### `credentials/updated` — emit + +Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Listener failures are contained and logged — a sync throw and an async rejection alike — without changing the committed operation's outcome, except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions. + +```ts cordis-catalog +/** + * Committed change to a provider-managed credential source: a `set`, an + * `unset`, or an external edit observed in storage. Ambient + * process-environment changes are not observable and never emit. Listener + * failures are contained and logged — a sync throw and an async rejection + * alike — without changing the committed operation's outcome, except + * `INVARIANT`-coded failures, which rethrow after every listener ran; + * that rethrow reaches the emitter only from synchronous listeners, so + * invariant checks on this event must not be async functions. + * @param ref - the reference whose stored value changed. + * @mode emit + */ +'credentials/updated'(ref: CredentialRef): void +``` + +Source: [`packages/credentials/credentials/src/index.ts:67`](../../packages/credentials/credentials/src/index.ts) + diff --git a/docs/subsystems/credentials.zh.md b/docs/subsystems/credentials.zh.md index 09cf374a23..6f5ccd1572 100644 --- a/docs/subsystems/credentials.zh.md +++ b/docs/subsystems/credentials.zh.md @@ -48,3 +48,86 @@ interface CredentialInfo { ## 变更提交 `credentials/updated (ref)` 在 provider 管理的来源发生已提交变更后触发——`set`、`unset` 或在存储中观察到的外部编辑。进程环境自身的变化不可观测,永不发出事件。消费方不需要该事件(它们按操作重新解析);它服务于配置界面刷新「已配置」徽标。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.credentials` — `Credentials` (abstract seam) + +Abstract credential service. Providers implement the four operations over their source layers; one seam-wide rule binds them all: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret. + +```ts cordis-catalog +/** + * Resolve one reference to its current value. Resolution is per call: + * consumers re-resolve at each operation and must not cache across + * operations — that per-operation read is what makes a changed credential + * reach the next operation without a restart. + * @param ref - the reference to resolve. + * @returns the value and its source, or `undefined` while unconfigured. + */ +abstract resolve(ref: CredentialRef): Promise + +/** + * Describe one reference for configuration surfaces without exposing the + * value. + * @param ref - the reference to describe. + * @returns configured state, supplying source, and writability. + */ +abstract describe(ref: CredentialRef): Promise + +/** + * Durably store one value in the provider-managed writable source. Rejects + * while a read-only source shadows the reference — the write would appear + * to succeed while resolution keeps returning the shadowing value — and + * rejects an empty value (use {@link unset}). + * @param ref - the reference to store. + * @param value - the non-empty secret value. + */ +abstract set(ref: CredentialRef, value: string): Promise + +/** + * Remove one reference from the provider-managed writable source; removing + * an absent reference is a no-op. Rejects while a read-only source shadows + * the reference, like {@link set}. + * @param ref - the reference to remove. + */ +abstract unset(ref: CredentialRef): Promise +``` + +Source: [`packages/credentials/credentials/src/index.ts:77`](../../packages/credentials/credentials/src/index.ts) + + + +### `credentials/*` events + + + +#### `credentials/updated` — emit + +Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Listener failures are contained and logged — a sync throw and an async rejection alike — without changing the committed operation's outcome, except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions. + +```ts cordis-catalog +/** + * Committed change to a provider-managed credential source: a `set`, an + * `unset`, or an external edit observed in storage. Ambient + * process-environment changes are not observable and never emit. Listener + * failures are contained and logged — a sync throw and an async rejection + * alike — without changing the committed operation's outcome, except + * `INVARIANT`-coded failures, which rethrow after every listener ran; + * that rethrow reaches the emitter only from synchronous listeners, so + * invariant checks on this event must not be async functions. + * @param ref - the reference whose stored value changed. + * @mode emit + */ +'credentials/updated'(ref: CredentialRef): void +``` + +Source: [`packages/credentials/credentials/src/index.ts:67`](../../packages/credentials/credentials/src/index.ts) + diff --git a/docs/subsystems/filesystem.i18n.yaml b/docs/subsystems/filesystem.i18n.yaml index 2f9478a7ba..cadc2b92d8 100644 --- a/docs/subsystems/filesystem.i18n.yaml +++ b/docs/subsystems/filesystem.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/filesystem.md -filesystem.md: addded9f673ed435e95109d4fb772967514c0b87 -filesystem.zh.md: 1e378928ed570b97dbec73b836a7e6ff18726ff8 +# pnpm run verify-translation-pairing --write docs/subsystems/filesystem.md +filesystem.md: 1999399254fa5d72ab222a42a2fa8f4b890da50a +filesystem.zh.md: 044a1c5be91ed8bec5d0442cc6f7edfde6870785 diff --git a/docs/subsystems/filesystem.md b/docs/subsystems/filesystem.md index addded9f67..1999399254 100644 --- a/docs/subsystems/filesystem.md +++ b/docs/subsystems/filesystem.md @@ -181,7 +181,7 @@ interface FsEditOutcome { `dsh-fs` owns three events the tool dispatches and the policy plugin listens for, so the emitter (`dsh-tool-fs`) and the listener (`dsh-fs-policy`) share a vocabulary without the emitter depending on the policy plugin. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure. -`fs/write-intent` and `fs/edit-intent` are **single-slot decision waterfalls**: the tool dispatches each with a default thunk returning `undefined` (the bare provider), and a listener fully decides without calling `next()`. The slot is first-wins by registration order — the policy plugin owning it is a deployment convention, not an enforced invariant. `fs/observed` is a fire-and-forget recording event dispatched with a plain `ctx.emit`; its listener MUST be synchronous and side-effect-only, because the tool does NOT guard the emit — a throwing listener would surface as the tool's `isError` result for a mutation that already succeeded. The generated catalog shows the exact signatures on [events.md](../cordis-catalog/events.md). +`fs/write-intent` and `fs/edit-intent` are **single-slot decision waterfalls**: the tool dispatches each with a default thunk returning `undefined` (the bare provider), and a listener fully decides without calling `next()`. The slot is first-wins by registration order — the policy plugin owning it is a deployment convention, not an enforced invariant. `fs/observed` is a fire-and-forget recording event dispatched with a plain `ctx.emit`; its listener MUST be synchronous and side-effect-only, because the tool does NOT guard the emit — a throwing listener would surface as the tool's `isError` result for a mutation that already succeeded. The generated [cordis surface](#cordis-surface) below shows the exact signatures. ## Execution context (policy plugin) @@ -258,4 +258,208 @@ type FsErrorCode = ## The service and the plugin -`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls (supplying `createIfAbsent`/`replaceIfVersion`/`{ version }` or throwing `FS_NOT_OBSERVED`) and records on `fs/observed`. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated wiring catalog shows the exact `ctx.fs` signatures on [services.md](../cordis-catalog/services.md#ctxfs--filesystem-abstract-seam). +`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls (supplying `createIfAbsent`/`replaceIfVersion`/`{ version }` or throwing `FS_NOT_OBSERVED`) and records on `fs/observed`. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated [`ctx.fs` section](#ctxfs--filesystem-abstract-seam) below shows the exact signatures. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.fs` — `FileSystem` (abstract seam) + +Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract. + +```ts cordis-catalog +/** + * Resolve a model/plugin-supplied path into a stable {@link FsTarget}. May perform I/O (a + * remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence + * async even though the local backend only normalizes + realpaths. + * + * @param path - the path to resolve; relative paths resolve against `opts.cwd`. + * @param opts - optional cwd override and cancellation signal. + * @returns the stable target; the same file yields the same `targetKey`. + */ +abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise + +/** + * Return the canonical absolute path a subprocess in this filesystem's + * execution world can open. The path is deliberately separate from + * {@link FsTarget.targetKey}: consumers may pass this value to another OS + * capability, but must continue treating the target key as opaque. + * @param target - the resolved target whose process path is required. + * @returns an absolute path in the backend's execution world. + */ +abstract processPath(target: FsTarget): string + +/** + * Return the canonical `file:` URI for a target in this filesystem's + * execution world. Backends own URI encoding because the host platform may + * differ from the execution platform. + * @param target - the resolved target to encode. + * @returns the target's canonical file URI. + */ +abstract fileUrl(target: FsTarget): string + +/** + * Test canonical containment without exposing or parsing backend target + * keys. Both targets must come from this provider. + * @param parent - canonical directory target. + * @param child - canonical candidate target. + * @returns true when `child` is `parent` or a descendant of it. + */ +abstract contains(parent: FsTarget, child: FsTarget): boolean + +/** + * Return target metadata, or `undefined` when the target does not exist. + * @param target - the resolved target to stat. + * @param signal - aborts the metadata round-trip. + * @returns metadata only, never content; undefined for an absent target. + */ +abstract stat(target: FsTarget, signal?: AbortSignal): Promise + +/** + * Return path metadata without following the final path component when it is a + * symbolic link. This is intentionally path-shaped, not target-shaped: + * {@link resolve} follows symlinks to produce the stable identity used by + * normal reads/writes, while `lstat` lets a consumer reject the path itself + * before that follow happens. + * + * `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is + * absent. + * @param path - the path to inspect; relative paths resolve against `opts.cwd`. + * @param opts - `cwd` overrides the backend's default base for relative paths. + * @param signal - aborts the metadata round-trip. + * @returns metadata only, never content; undefined for an absent path. + */ +abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise + +/** + * Read the whole regular text file as a single decoded string. + * @param target - the resolved target to read. + * @param signal - aborts the read. + * @returns the full decoded UTF-8 content. + */ +abstract readText(target: FsTarget, signal?: AbortSignal): Promise + +/** + * Stream the whole regular text file as decoded text chunks (same text + * semantics as {@link readText}, for large files). The backend owns + * cross-chunk UTF-8 decoding and binary rejection so the policy layer never + * touches raw bytes. + * @param target - the resolved target to read. + * @param signal - aborts the stream, including between chunks. + * @returns the chunk iterable, decoded and validated like {@link readText}. + */ +abstract streamText(target: FsTarget, signal?: AbortSignal): Promise> + +/** + * List direct children of a directory in stable name order. Returns resolved + * child targets plus cheap metadata only; never reads file contents. + * @param target - the resolved directory target. + * @param signal - aborts the listing. + * @returns one entry per direct child, in stable name order. + */ +abstract listDir(target: FsTarget, signal?: AbortSignal): Promise + +/** + * Atomically create or replace UTF-8 text. `expected` guards intent and + * staleness; omission allows unconditional overwrite. + * @param target - the resolved target to write. + * @param content - the full new file content. + * @param expected - the write intent guarding the write; omit for unconditional. + * @param signal - aborts before the atomic rename takes effect. + * @param sandboxPolicy - the per-call mode and workspace root this write + * runs under; a sandboxing backend fences the write by it, the bare backend + * ignores it. Omit to leave the backend its own default. + * @returns the outcome, including the version the write produced. + */ +abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise + +/** + * Atomically edit literal text. When supplied, the version guard is checked + * before matching so stale content reports `FS_STALE_VERSION`; omission edits + * the current content without a freshness precondition. + * @param target - the resolved target to edit. + * @param edit - the literal search/replace request. + * @param expected - the version guard; omit for an unconditional edit. + * @param signal - aborts before the atomic rename takes effect. + * @param sandboxPolicy - the per-call mode and workspace root this edit runs + * under; a sandboxing backend fences the edit by it, the bare backend + * ignores it. Omit to leave the backend its own default. + * @returns the outcome, including the version the edit produced. + */ +abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise +``` + +Types: [SandboxExecutionPolicy](sandbox.md) + +Source: [`packages/fs/fs/src/index.ts:83`](../../packages/fs/fs/src/index.ts) + + + +### `fs/*` events + + + +#### `fs/edit-intent` — waterfall + +Single-slot decision for the next FileSystem.editText. Calling `next()` yields an unconditional edit; the first returned guard wins. + +```ts cordis-catalog +/** + * Single-slot decision for the next {@link FileSystem.editText}. Calling + * `next()` yields an unconditional edit; the first returned guard wins. + * @param target - the resolved target about to be edited. + * @param actor - the opaque tool-execution context the decider keys off. + * @mode waterfall + */ +'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined> +``` + +Source: [`packages/fs/fs/src/index.ts:64`](../../packages/fs/fs/src/index.ts) + + + +#### `fs/observed` — emit + +Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited. + +```ts cordis-catalog +/** + * Record a successful observation. Listeners must be synchronous recorders: + * throws fail the tool call and returned promises are not awaited. + * @param target - the target that was read/written/edited. + * @param version - the version the actor now holds as its observation. + * @param actor - the observing tool-execution context; undefined records nothing useful. + * @mode emit + */ +'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void +``` + +Source: [`packages/fs/fs/src/index.ts:73`](../../packages/fs/fs/src/index.ts) + + + +#### `fs/write-intent` — waterfall + +Single-slot decision for the next FileSystem.writeText. Calling `next()` yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers. + +```ts cordis-catalog +/** + * Single-slot decision for the next {@link FileSystem.writeText}. Calling + * `next()` yields the bare provider's unconditional write; the first listener + * that returns an intent owns the decision rather than composing with peers. + * @param target - the resolved target about to be written. + * @param actor - the opaque tool-execution context the decider keys off. + * @mode waterfall + */ +'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise): Promise +``` + +Source: [`packages/fs/fs/src/index.ts:56`](../../packages/fs/fs/src/index.ts) + diff --git a/docs/subsystems/filesystem.zh.md b/docs/subsystems/filesystem.zh.md index 1e378928ed..044a1c5be9 100644 --- a/docs/subsystems/filesystem.zh.md +++ b/docs/subsystems/filesystem.zh.md @@ -181,7 +181,7 @@ interface FsEditOutcome { `dsh-fs` 拥有三个事件,由工具分发、策略插件监听,使发射方(`dsh-tool-fs`)与监听方(`dsh-fs-policy`)共享词汇,而发射方无需依赖策略插件。它们只携带 `dsh-fs` 词汇加一个不透明的 `object` actor,不含面向模型的概念,也不含 agent/会话所有者结构。 -`fs/write-intent` 与 `fs/edit-intent` 是**单槽决策 waterfall**:工具分发时附带一个默认 thunk(返回 `undefined`,即裸提供方),监听方完全决策而不调用 `next()`。该槽按注册顺序先到先得——由策略插件占据是部署约定,而非强制不变式。`fs/observed` 是一个即发即弃的记录事件,通过普通 `ctx.emit` 分发;其监听方必须是同步的、仅产生副作用,因为工具不会捕获该 emit 抛出的异常——抛出异常的监听方会导致工具为一次已经成功的变更返回 `isError` 结果。生成的目录在 [events.md](../cordis-catalog/events.md) 中展示确切签名。 +`fs/write-intent` 与 `fs/edit-intent` 是**单槽决策 waterfall**:工具分发时附带一个默认 thunk(返回 `undefined`,即裸提供方),监听方完全决策而不调用 `next()`。该槽按注册顺序先到先得——由策略插件占据是部署约定,而非强制不变式。`fs/observed` 是一个即发即弃的记录事件,通过普通 `ctx.emit` 分发;其监听方必须是同步的、仅产生副作用,因为工具不会捕获该 emit 抛出的异常——抛出异常的监听方会导致工具为一次已经成功的变更返回 `isError` 结果。下方生成的 [cordis surface](#cordis-surface) 展示确切签名。 ## 执行上下文(策略插件) @@ -258,4 +258,208 @@ type FsErrorCode = ## 服务与插件 -`FileSystem`(`ctx.fs`,abstract)拥有提供方原语:`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:对写入/编辑意图 waterfall 作出决策(提供 `createIfAbsent`/`replaceIfVersion`/`{ version }`,或抛出 `FS_NOT_OBSERVED`),并在 `fs/observed` 上记录。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall,并 emit 记录事件。生成的 wiring 目录在 [services.md](../cordis-catalog/services.md#ctxfs--filesystem-abstract-seam) 中展示确切的 `ctx.fs` 签名。 +`FileSystem`(`ctx.fs`,abstract)拥有提供方原语:`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:对写入/编辑意图 waterfall 作出决策(提供 `createIfAbsent`/`replaceIfVersion`/`{ version }`,或抛出 `FS_NOT_OBSERVED`),并在 `fs/observed` 上记录。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall,并 emit 记录事件。下方生成的 [`ctx.fs` 小节](#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.fs` — `FileSystem` (abstract seam) + +Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract. + +```ts cordis-catalog +/** + * Resolve a model/plugin-supplied path into a stable {@link FsTarget}. May perform I/O (a + * remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence + * async even though the local backend only normalizes + realpaths. + * + * @param path - the path to resolve; relative paths resolve against `opts.cwd`. + * @param opts - optional cwd override and cancellation signal. + * @returns the stable target; the same file yields the same `targetKey`. + */ +abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise + +/** + * Return the canonical absolute path a subprocess in this filesystem's + * execution world can open. The path is deliberately separate from + * {@link FsTarget.targetKey}: consumers may pass this value to another OS + * capability, but must continue treating the target key as opaque. + * @param target - the resolved target whose process path is required. + * @returns an absolute path in the backend's execution world. + */ +abstract processPath(target: FsTarget): string + +/** + * Return the canonical `file:` URI for a target in this filesystem's + * execution world. Backends own URI encoding because the host platform may + * differ from the execution platform. + * @param target - the resolved target to encode. + * @returns the target's canonical file URI. + */ +abstract fileUrl(target: FsTarget): string + +/** + * Test canonical containment without exposing or parsing backend target + * keys. Both targets must come from this provider. + * @param parent - canonical directory target. + * @param child - canonical candidate target. + * @returns true when `child` is `parent` or a descendant of it. + */ +abstract contains(parent: FsTarget, child: FsTarget): boolean + +/** + * Return target metadata, or `undefined` when the target does not exist. + * @param target - the resolved target to stat. + * @param signal - aborts the metadata round-trip. + * @returns metadata only, never content; undefined for an absent target. + */ +abstract stat(target: FsTarget, signal?: AbortSignal): Promise + +/** + * Return path metadata without following the final path component when it is a + * symbolic link. This is intentionally path-shaped, not target-shaped: + * {@link resolve} follows symlinks to produce the stable identity used by + * normal reads/writes, while `lstat` lets a consumer reject the path itself + * before that follow happens. + * + * `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is + * absent. + * @param path - the path to inspect; relative paths resolve against `opts.cwd`. + * @param opts - `cwd` overrides the backend's default base for relative paths. + * @param signal - aborts the metadata round-trip. + * @returns metadata only, never content; undefined for an absent path. + */ +abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise + +/** + * Read the whole regular text file as a single decoded string. + * @param target - the resolved target to read. + * @param signal - aborts the read. + * @returns the full decoded UTF-8 content. + */ +abstract readText(target: FsTarget, signal?: AbortSignal): Promise + +/** + * Stream the whole regular text file as decoded text chunks (same text + * semantics as {@link readText}, for large files). The backend owns + * cross-chunk UTF-8 decoding and binary rejection so the policy layer never + * touches raw bytes. + * @param target - the resolved target to read. + * @param signal - aborts the stream, including between chunks. + * @returns the chunk iterable, decoded and validated like {@link readText}. + */ +abstract streamText(target: FsTarget, signal?: AbortSignal): Promise> + +/** + * List direct children of a directory in stable name order. Returns resolved + * child targets plus cheap metadata only; never reads file contents. + * @param target - the resolved directory target. + * @param signal - aborts the listing. + * @returns one entry per direct child, in stable name order. + */ +abstract listDir(target: FsTarget, signal?: AbortSignal): Promise + +/** + * Atomically create or replace UTF-8 text. `expected` guards intent and + * staleness; omission allows unconditional overwrite. + * @param target - the resolved target to write. + * @param content - the full new file content. + * @param expected - the write intent guarding the write; omit for unconditional. + * @param signal - aborts before the atomic rename takes effect. + * @param sandboxPolicy - the per-call mode and workspace root this write + * runs under; a sandboxing backend fences the write by it, the bare backend + * ignores it. Omit to leave the backend its own default. + * @returns the outcome, including the version the write produced. + */ +abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise + +/** + * Atomically edit literal text. When supplied, the version guard is checked + * before matching so stale content reports `FS_STALE_VERSION`; omission edits + * the current content without a freshness precondition. + * @param target - the resolved target to edit. + * @param edit - the literal search/replace request. + * @param expected - the version guard; omit for an unconditional edit. + * @param signal - aborts before the atomic rename takes effect. + * @param sandboxPolicy - the per-call mode and workspace root this edit runs + * under; a sandboxing backend fences the edit by it, the bare backend + * ignores it. Omit to leave the backend its own default. + * @returns the outcome, including the version the edit produced. + */ +abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise +``` + +Types: [SandboxExecutionPolicy](sandbox.md) + +Source: [`packages/fs/fs/src/index.ts:83`](../../packages/fs/fs/src/index.ts) + + + +### `fs/*` events + + + +#### `fs/edit-intent` — waterfall + +Single-slot decision for the next FileSystem.editText. Calling `next()` yields an unconditional edit; the first returned guard wins. + +```ts cordis-catalog +/** + * Single-slot decision for the next {@link FileSystem.editText}. Calling + * `next()` yields an unconditional edit; the first returned guard wins. + * @param target - the resolved target about to be edited. + * @param actor - the opaque tool-execution context the decider keys off. + * @mode waterfall + */ +'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined> +``` + +Source: [`packages/fs/fs/src/index.ts:64`](../../packages/fs/fs/src/index.ts) + + + +#### `fs/observed` — emit + +Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited. + +```ts cordis-catalog +/** + * Record a successful observation. Listeners must be synchronous recorders: + * throws fail the tool call and returned promises are not awaited. + * @param target - the target that was read/written/edited. + * @param version - the version the actor now holds as its observation. + * @param actor - the observing tool-execution context; undefined records nothing useful. + * @mode emit + */ +'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void +``` + +Source: [`packages/fs/fs/src/index.ts:73`](../../packages/fs/fs/src/index.ts) + + + +#### `fs/write-intent` — waterfall + +Single-slot decision for the next FileSystem.writeText. Calling `next()` yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers. + +```ts cordis-catalog +/** + * Single-slot decision for the next {@link FileSystem.writeText}. Calling + * `next()` yields the bare provider's unconditional write; the first listener + * that returns an intent owns the decision rather than composing with peers. + * @param target - the resolved target about to be written. + * @param actor - the opaque tool-execution context the decider keys off. + * @mode waterfall + */ +'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise): Promise +``` + +Source: [`packages/fs/fs/src/index.ts:56`](../../packages/fs/fs/src/index.ts) + diff --git a/docs/subsystems/goal.i18n.yaml b/docs/subsystems/goal.i18n.yaml index 625556f99a..945ed658d2 100644 --- a/docs/subsystems/goal.i18n.yaml +++ b/docs/subsystems/goal.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/goal.md -goal.md: fc6a7e63e58fc7cd4bc524be1e66515593680d95 -goal.zh.md: c584cb375bd55c13c158e7ee22d7721040dffa05 +# pnpm run verify-translation-pairing --write docs/subsystems/goal.md +goal.md: 6f54a5261cb44c3fda389e37cb689a00061ab241 +goal.zh.md: 043263f3231b4c727f21ca0b8fc07d8adffa0559 diff --git a/docs/subsystems/goal.md b/docs/subsystems/goal.md index fc6a7e63e5..6f54a5261c 100644 --- a/docs/subsystems/goal.md +++ b/docs/subsystems/goal.md @@ -143,3 +143,135 @@ interface GoalChanged { ## Service behavior [`GoalService`](../../packages/goal/goal/src/index.ts) resolves creation defaults, folds strict replay from durable `goal/change` events, enforces exact-live-agent identity and compare-and-set mutations, and emits contained `goal/changed` notifications. The package [README](../../packages/goal/goal/README.md) owns the callable and model-visible contract. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.goals` — `GoalService` + +Goal service (`ctx.goals`) backed exclusively by the owning session log. + +```ts cordis-catalog +/** + * Read the current goal for one exact live agent. + * @param agent - owning live agent. + * @returns a fresh view or `undefined` when no goal is current. + * @throws {@link GoalError} when the agent is not the registry's live instance. + */ +get(agent: Agent): GoalView | undefined + +/** + * Remove process-local continuation authority without changing durable goal + * phase or revision. Lifecycle owners use this before unloading a driver; + * a later human-authorized {@link resume} records the new activation edge. + * @param agent - owning live agent. + * @returns a fresh disarmed view, or `undefined` when no goal is current. + */ +disarm(agent: Agent): GoalView | undefined + +/** + * Create and arm a goal. A completed goal may be replaced; every other + * current phase must be cleared or resumed instead. + * @param agent - owning live agent. + * @param request - objective and optional round cap. + * @returns the created live view. + */ +create(agent: Agent, request: CreateGoalRequest): GoalView + +/** + * Edit objective and/or round cap without changing phase. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @param request - at least one replacement field. + * @returns the edited view. + */ +@Remote('edit') edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView + +/** + * Pause an active goal and disarm automatic continuation. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @returns the paused view. + */ +@Remote('pause') pause(agent: Agent, ref: GoalRef): GoalView + +/** + * Resume and arm a stopped goal, or rearm an active goal after a + * session-start edge, while its round budget still has capacity. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @returns the active view. + */ +@Remote('resume') resume(agent: Agent, ref: GoalRef): GoalView + +/** + * Mark a current non-complete goal complete and disarm it. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @returns the completed view. + */ +@Remote('complete') complete(agent: Agent, ref: GoalRef): GoalView + +/** + * Mark an active goal blocked and disarm it. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @param reason - policy-owned stable code and human-readable explanation. + * @returns the blocked view with its durable reason. + */ +block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView + +/** + * Clear the current goal while retaining a durable tombstone and history. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @returns the tombstone ref whose revision is one past the cleared snapshot. + */ +@Remote('clear') clear(agent: Agent, ref: GoalRef): GoalRef + +/** + * Create one Goal through the remote boundary. + * @param agent - exact live Agent resolved from the wire identity. + * @param request - objective and optional round cap. + * @returns the created Goal identity. + */ +@Remote('create') remoteExportCreate(agent: Agent, request: CreateGoalRequest): CreateGoalResult +``` + +Types: [Agent](core.md) + +Source: [`packages/goal/goal/src/index.ts:183`](../../packages/goal/goal/src/index.ts) + + + +### `goal/*` events + + + +#### `goal/changed` — emit + +Goal mutation accepted by one live agent. The matching `goal/change` session event has already committed. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + +```ts cordis-catalog +/** + * Goal mutation accepted by one live agent. The matching `goal/change` + * session event has already committed. Listener failures are contained. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @param payload.agent - agent whose session owns the goal. + * @param payload.change - fresh current projection or clear tombstone. + * @mode emit + */ +'goal/changed'(this: import('@deepseek-ai/dsh-scope').Scoped, payload: { agent: Agent; change: GoalChanged }): void +``` + +Types: [Agent](core.md) · [Scoped](scope.md) + +Source: [`packages/goal/goal/src/domain.ts:114`](../../packages/goal/goal/src/domain.ts) + diff --git a/docs/subsystems/goal.zh.md b/docs/subsystems/goal.zh.md index c584cb375b..043263f323 100644 --- a/docs/subsystems/goal.zh.md +++ b/docs/subsystems/goal.zh.md @@ -143,3 +143,135 @@ interface GoalChanged { ## 服务行为 [`GoalService`](../../packages/goal/goal/src/index.ts) 解析创建默认值、从持久 `goal/change` 事件执行严格回放折叠、校验确切的活跃 agent 身份、以比较并设置方式执行变更,并发出 `goal/changed` 通知;监听器故障会被隔离。包 [README](../../packages/goal/goal/README.md) 负责记录可调用契约和面向模型的契约。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.goals` — `GoalService` + +Goal service (`ctx.goals`) backed exclusively by the owning session log. + +```ts cordis-catalog +/** + * Read the current goal for one exact live agent. + * @param agent - owning live agent. + * @returns a fresh view or `undefined` when no goal is current. + * @throws {@link GoalError} when the agent is not the registry's live instance. + */ +get(agent: Agent): GoalView | undefined + +/** + * Remove process-local continuation authority without changing durable goal + * phase or revision. Lifecycle owners use this before unloading a driver; + * a later human-authorized {@link resume} records the new activation edge. + * @param agent - owning live agent. + * @returns a fresh disarmed view, or `undefined` when no goal is current. + */ +disarm(agent: Agent): GoalView | undefined + +/** + * Create and arm a goal. A completed goal may be replaced; every other + * current phase must be cleared or resumed instead. + * @param agent - owning live agent. + * @param request - objective and optional round cap. + * @returns the created live view. + */ +create(agent: Agent, request: CreateGoalRequest): GoalView + +/** + * Edit objective and/or round cap without changing phase. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @param request - at least one replacement field. + * @returns the edited view. + */ +@Remote('edit') edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView + +/** + * Pause an active goal and disarm automatic continuation. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @returns the paused view. + */ +@Remote('pause') pause(agent: Agent, ref: GoalRef): GoalView + +/** + * Resume and arm a stopped goal, or rearm an active goal after a + * session-start edge, while its round budget still has capacity. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @returns the active view. + */ +@Remote('resume') resume(agent: Agent, ref: GoalRef): GoalView + +/** + * Mark a current non-complete goal complete and disarm it. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @returns the completed view. + */ +@Remote('complete') complete(agent: Agent, ref: GoalRef): GoalView + +/** + * Mark an active goal blocked and disarm it. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @param reason - policy-owned stable code and human-readable explanation. + * @returns the blocked view with its durable reason. + */ +block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView + +/** + * Clear the current goal while retaining a durable tombstone and history. + * @param agent - owning live agent. + * @param ref - expected current revision. + * @returns the tombstone ref whose revision is one past the cleared snapshot. + */ +@Remote('clear') clear(agent: Agent, ref: GoalRef): GoalRef + +/** + * Create one Goal through the remote boundary. + * @param agent - exact live Agent resolved from the wire identity. + * @param request - objective and optional round cap. + * @returns the created Goal identity. + */ +@Remote('create') remoteExportCreate(agent: Agent, request: CreateGoalRequest): CreateGoalResult +``` + +Types: [Agent](core.md) + +Source: [`packages/goal/goal/src/index.ts:183`](../../packages/goal/goal/src/index.ts) + + + +### `goal/*` events + + + +#### `goal/changed` — emit + +Goal mutation accepted by one live agent. The matching `goal/change` session event has already committed. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + +```ts cordis-catalog +/** + * Goal mutation accepted by one live agent. The matching `goal/change` + * session event has already committed. Listener failures are contained. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @param payload.agent - agent whose session owns the goal. + * @param payload.change - fresh current projection or clear tombstone. + * @mode emit + */ +'goal/changed'(this: import('@deepseek-ai/dsh-scope').Scoped, payload: { agent: Agent; change: GoalChanged }): void +``` + +Types: [Agent](core.md) · [Scoped](scope.md) + +Source: [`packages/goal/goal/src/domain.ts:114`](../../packages/goal/goal/src/domain.ts) + diff --git a/docs/subsystems/http-server.i18n.yaml b/docs/subsystems/http-server.i18n.yaml index 6f1facfad7..3674a95c44 100644 --- a/docs/subsystems/http-server.i18n.yaml +++ b/docs/subsystems/http-server.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/http-server.md -http-server.md: 200d3eed683f203b65824ea3f62cf56ce3df912f -http-server.zh.md: 9a12d3769a93270810927075be5f236875688b91 +http-server.md: b9795fe98b432b6ef5f7d01a4d3e115c809fe642 +http-server.zh.md: c3ba02dce5c58f491921f1fb31f999f9da808bc5 diff --git a/docs/subsystems/http-server.md b/docs/subsystems/http-server.md index 200d3eed68..b9795fe98b 100644 --- a/docs/subsystems/http-server.md +++ b/docs/subsystems/http-server.md @@ -45,3 +45,64 @@ interface Config { `HttpServerService` (`ctx.httpServer`) listens immediately on activation; a listen failure (EADDRINUSE…) throws out of init — a FAILED fiber the boot's fail-loud sweep reports. `register(route)` adds one named route and returns its disposer; a duplicate `(kind, path)` throws, because route patterns are a composition-level contract and a collision is a misconfiguration. `tapIndex(transform)` adds a pure html-to-html transform applied to every index response — `/` and each SPA fallback — in registration order; [dsh-client-modules](../../packages/client/modules) uses it to inject the boot manifest. `port` reads the listening port, the OS-assigned value when `config.port` is 0. A request whose handling throws (a malformed %-escape hitting `decodeURIComponent`, a client dropping mid-body) is logged as a warning and answered 400 — or the socket destroyed when headers are already out — never a process exit. Disposal pairs `close()` with `closeAllConnections()` because a handler may hold its response open (SSE) and such connections never end on their own; without the force-close, teardown would hang. The package never prints: the URL line belongs to the shell. Per-package operational detail, including the dev-mode bundle watch pipeline, stays in the [README](../../packages/host/webserver/README.md). + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.httpServer` — `HttpServerService` + +The web-shape HTTP carrier service. Activation listens immediately (route registration order carries no request-facing semantics: named routes are composed to be disjoint, and the fallback seat answers anything not yet claimed during the boot window — 404 until its owner registers). A listen failure throws out of init — a FAILED fiber the boot's fail-loud sweep reports. + +```ts cordis-catalog +/** + * Register a named route. Duplicate (kind, path) throws — route patterns are + * a composition-level contract, so a collision is a misconfiguration. + * @param route - kind, path, and the owning handler. + * @returns the disposer removing the route. + */ +register(route: WebRoute): () => void + +/** + * Register an exact-path HTTP upgrade route. Duplicate paths throw because + * one socket can have only one protocol owner. + * @param route - pathname and handler owning negotiation plus socket use. + * @returns the disposer removing the route. + */ +registerUpgrade(route: WebUpgradeRoute): () => void + +/** + * Claim the fallback seat: the handler answering every request no named + * route matches (the SPA dist server in the shipped Web composition). One + * owner only — a second registration throws, because two fallbacks cannot + * compose. + * @param handler - owns the full response lifecycle of unmatched requests. + * @returns the disposer releasing the seat. + */ +registerFallback(handler: WebRoute['handler']): () => void + +/** + * Register an index.html transform, applied by the fallback owner to every + * index response ({@link applyIndexTaps}) in registration order. + * @param transform - pure html-to-html function. + * @returns the disposer removing the transform. + */ +tapIndex(transform: (html: string) => string): () => void + +/** + * Run an index.html body through the registered taps in registration order + * — called by the fallback owner on every index response it renders. + * @param html - the raw index.html body. + * @returns the transformed body. + */ +applyIndexTaps(html: string): string +``` + +Source: [`packages/host/webserver/src/index.ts:60`](../../packages/host/webserver/src/index.ts) + diff --git a/docs/subsystems/http-server.zh.md b/docs/subsystems/http-server.zh.md index 9a12d3769a..c3ba02dce5 100644 --- a/docs/subsystems/http-server.zh.md +++ b/docs/subsystems/http-server.zh.md @@ -45,3 +45,64 @@ interface Config { `HttpServerService`(`ctx.httpServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会从 init 抛出,形成一个 FAILED fiber,由启动的大声失败 sweep 上报。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层契约,冲突即配置错误。`tapIndex(transform)` 添加一个纯的 html 到 html 转换,按注册顺序应用于每个 index 响应(`/` 和每次 SPA 回退);[dsh-client-modules](../../packages/client/modules) 用它注入启动 manifest(元数据清单)。`port` 读取监听端口,`config.port` 为 0 时读到的是操作系统分配的值。 处理过程中抛出异常的请求(畸形的 % 转义撞上 `decodeURIComponent`、客户端在请求体中途断开)会记录为警告并应答 400(响应头已发出时则销毁 socket),绝不导致进程退出。dispose(资源释放)把 `close()` 与 `closeAllConnections()` 配对使用,因为处理器可能像 SSE(Server-Sent Events)那样保持响应打开,而这类连接永远不会自行结束;没有强制关闭,拆卸就会挂起。该包(package)从不打印输出:URL 行归 shell 所有。逐包运维细节(含开发模式的 bundle 监视流水线)留在 [README](../../packages/host/webserver/README.md) 中。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.httpServer` — `HttpServerService` + +The web-shape HTTP carrier service. Activation listens immediately (route registration order carries no request-facing semantics: named routes are composed to be disjoint, and the fallback seat answers anything not yet claimed during the boot window — 404 until its owner registers). A listen failure throws out of init — a FAILED fiber the boot's fail-loud sweep reports. + +```ts cordis-catalog +/** + * Register a named route. Duplicate (kind, path) throws — route patterns are + * a composition-level contract, so a collision is a misconfiguration. + * @param route - kind, path, and the owning handler. + * @returns the disposer removing the route. + */ +register(route: WebRoute): () => void + +/** + * Register an exact-path HTTP upgrade route. Duplicate paths throw because + * one socket can have only one protocol owner. + * @param route - pathname and handler owning negotiation plus socket use. + * @returns the disposer removing the route. + */ +registerUpgrade(route: WebUpgradeRoute): () => void + +/** + * Claim the fallback seat: the handler answering every request no named + * route matches (the SPA dist server in the shipped Web composition). One + * owner only — a second registration throws, because two fallbacks cannot + * compose. + * @param handler - owns the full response lifecycle of unmatched requests. + * @returns the disposer releasing the seat. + */ +registerFallback(handler: WebRoute['handler']): () => void + +/** + * Register an index.html transform, applied by the fallback owner to every + * index response ({@link applyIndexTaps}) in registration order. + * @param transform - pure html-to-html function. + * @returns the disposer removing the transform. + */ +tapIndex(transform: (html: string) => string): () => void + +/** + * Run an index.html body through the registered taps in registration order + * — called by the fallback owner on every index response it renders. + * @param html - the raw index.html body. + * @returns the transformed body. + */ +applyIndexTaps(html: string): string +``` + +Source: [`packages/host/webserver/src/index.ts:60`](../../packages/host/webserver/src/index.ts) + diff --git a/docs/subsystems/invariants.i18n.yaml b/docs/subsystems/invariants.i18n.yaml index 27b6ac8e87..8ef657b3a4 100644 --- a/docs/subsystems/invariants.i18n.yaml +++ b/docs/subsystems/invariants.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/invariants.md -invariants.md: 143a7feabbab18556f595a78c7aef17caa6d3028 -invariants.zh.md: fb497857c02202544edf44ccfe3a2935a988e30e +invariants.md: 184d982bb27ab3ff98f715f0751c8bfcc1d08bea +invariants.zh.md: bd2f83a018642832fe50ba5cca6dd82e2bd7d59b diff --git a/docs/subsystems/invariants.md b/docs/subsystems/invariants.md index 143a7feabb..184d982bb2 100644 --- a/docs/subsystems/invariants.md +++ b/docs/subsystems/invariants.md @@ -57,3 +57,32 @@ An enabled installer runs in a dedicated child Cordis fiber; `installer.inject` ## The companion contract Every workspace package owns a `./invariant` companion ([package contract](../../packages/AGENTS.md)); publication and registration are exhaustive, but assertions are deliberately not synthetic. A companion installs a check only when its package owns an observable event or mutable-data relationship; otherwise it exports an empty installer whose leading comment starts `No runtime invariant:` and explains, package-specifically, why nothing is checkable. `pnpm run verify-package-invariants` mechanically rejects generated markers, unexplained empty installers, non-empty installers that omit or ignore the reporter, incorrect registration names, and incomplete export, publication, dependency, or bundle wiring ([mechanical-rule Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md)). The catalog of executable companions and the standard composition live in the [package README](../../packages/support/invariants/README.md). + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.invariants` — `InvariantService` + +Package-owned invariant registry with global and regex-based selection. + +```ts cordis-catalog +/** + * Register one package's invariant installer. The package name is reserved + * even when filtering disables its checks. Enabled installers run in a child + * fiber; failure disposes that fiber and releases the reservation. + * @param packageName - full npm package name that owns the contribution. + * @param installer - listener or startup-check installer for the child context. + * @returns an effect-scoped disposer for the registration. + */ +register(packageName: string, installer: InvariantInstaller): () => void +``` + +Source: [`packages/support/invariants/src/index.ts:94`](../../packages/support/invariants/src/index.ts) + diff --git a/docs/subsystems/invariants.zh.md b/docs/subsystems/invariants.zh.md index fb497857c0..bd2f83a018 100644 --- a/docs/subsystems/invariants.zh.md +++ b/docs/subsystems/invariants.zh.md @@ -57,3 +57,32 @@ interface InvariantInstaller { ## 配套插件契约 每个工作区包都拥有一个 `./invariant` 配套插件([包契约](../../packages/AGENTS.md));发布与注册是穷尽式的,但刻意不合成断言。只有当包对某个可观察事件或可变数据拥有所有权关系时,配套插件才安装检查;否则它导出一个空安装器,其起始注释以 `No runtime invariant:` 开头,针对该包具体解释为什么没有可检查项。`pnpm run verify-package-invariants` 机械地拒绝「生成文件」标记、无解释的空安装器、遗漏或忽略报告器的非空安装器、错误的注册名称,以及不完整的导出、发布、依赖或打包接线([机械规则 Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md))。可执行配套插件的目录与标准组合方式见[包 README](../../packages/support/invariants/README.md)。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.invariants` — `InvariantService` + +Package-owned invariant registry with global and regex-based selection. + +```ts cordis-catalog +/** + * Register one package's invariant installer. The package name is reserved + * even when filtering disables its checks. Enabled installers run in a child + * fiber; failure disposes that fiber and releases the reservation. + * @param packageName - full npm package name that owns the contribution. + * @param installer - listener or startup-check installer for the child context. + * @returns an effect-scoped disposer for the registration. + */ +register(packageName: string, installer: InvariantInstaller): () => void +``` + +Source: [`packages/support/invariants/src/index.ts:94`](../../packages/support/invariants/src/index.ts) + diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index 1ff1a1d42b..bd1d833f51 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/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: 5c90b3ce4ac65a99997f6ba7ad5deac494b7f772 -llm-streaming.zh.md: 7fd0043234cb40d6b21cec6ff101993164785a6e +llm-streaming.md: fe4a831660dd81b33a1cd7dc9a9d7026f3831991 +llm-streaming.zh.md: a10b696ec0887e139785fc7e95d8641e6496de79 diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index 5c90b3ce4a..fe4a831660 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -255,3 +255,191 @@ interface ContentBlockMap { ``` See [core.md § Content blocks and messages](core.md#content-blocks-and-messages) for the block interfaces. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.llm` — `LlmService` + +The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall. + +```ts cordis-catalog +/** + * Register an adapter for the given provider routes. Throws `LlmError` with code + * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing). + * Disposed with the fiber. + * @param providers - every provider route this adapter should serve. + * @param adapter - the adapter that streams calls for those providers. + * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}. + */ +registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle + +/** + * Describe provider routes with a registered adapter. + * @returns detached provider metadata in registration order. + */ +listProviders(): LlmProviderInfo[] + +/** + * Declare provider routes an adapter plugin can activate through + * configuration. Registration is all-or-nothing: an empty list, invalid + * 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 a handle that withdraws all of them, and can atomically replace them. + */ +registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle + +/** + * List every declared configurable provider, registered or dormant. + * @returns detached directory entries in declaration order. + */ +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, ): () => 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 + +/** + * Resolve the retry policy captured when one provider route was registered. + * @param provider - registered provider route to inspect. + * @returns the provider-owned policy, with normal defaults already resolved. + */ +providerRetryPolicy(provider: string): ResolvedRetryPolicy + +/** + * Discover models advertised by one registered provider. Catalog membership + * is advisory and never changes routing or request validation. + * @param provider - registered provider route to inspect. + * @returns detached model metadata in adapter-preferred order. + */ +async listModels(provider: string): Promise + +/** + * Resolve and validate all metadata from the adapter that owns one exact + * route. The result is detached from adapter-owned objects; catalog + * membership remains advisory and does not control request routing. + * @param provider - registered provider route to inspect. + * @param model - exact model id passed to the adapter. + * @param signal - optional cancellation for adapter-owned asynchronous lookup. + * @returns exact model identity plus available context and reasoning metadata. + */ +async resolveModelInfo( provider: string, model: string, signal?: AbortSignal, ): Promise + +/** + * Validate a conversation call config against its exact model capability and + * materialize adapter-configured defaults. Unsupported explicit efforts + * reject before provider I/O; no clamping or aliasing is performed. This + * standalone query does not bind a later dispatch; use {@link prepareCall} + * when logging and streaming must share one adapter registration. + * @param config - provider/model route and optional request controls. + * @param signal - optional cancellation for adapter-owned capability lookup. + * @returns a detached config only when a default must be materialized. + */ +async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise + +/** + * Resolve one call under its current adapter registration. The returned + * one-shot handle keeps that registration across header logging and dispatch, + * so HMR cannot combine one adapter's capability result with another adapter. + * @param config - provider/model route and optional request controls. + * @param signal - optional cancellation for adapter-owned capability lookup. + * @returns a prepared config and its registration-bound stream entry point. + */ +async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise + +/** + * 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 +``` + +Types: [AdapterRegistrationHandle](core.md) · [DirectoryRegistrationHandle](core.md) · [GenerateOptions](core.md) · [LlmCallConfig](core.md) · [LlmConfigurableProvider](core.md) · [LlmDiscoveredModel](core.md) · [LlmModelDiscoveryRequest](core.md) · [LlmModelInfo](core.md) · [LlmProviderInfo](core.md) · [LlmResolvedModelInfo](core.md) + +Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts) + + + +### `llm/*` events + + + +#### `llm/adapters-updated` — emit + +The provider topology changed: an adapter registered or unregistered routes, or the configurable-provider directory gained or lost entries. This is a payload-free registry notification fired at each commit point (including registration disposal); consumers re-read `listProviders()`, `listModels()`, or `listConfigurableProviders()` for the new state. Observer failures are contained and cannot veto the registry mutation. + +```ts cordis-catalog +/** + * The provider topology changed: an adapter registered or unregistered + * routes, or the configurable-provider directory gained or lost entries. + * This is a payload-free registry notification fired at each commit point + * (including registration disposal); consumers re-read `listProviders()`, + * `listModels()`, or `listConfigurableProviders()` for the new state. + * Observer failures are contained and cannot veto the registry mutation. + * @mode emit + */ +'llm/adapters-updated'(): void +``` + +Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts) + + + +#### `llm/stream` — waterfall + +Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit. + +```ts cordis-catalog +/** + * Waterfall around every streaming model call (retry, replay, routing). + * Bound to the {@link LlmService}; call `next()` to reach the resolved + * adapter's stream, or yield your own chunks to short-circuit. + * @param options - the full request. A LOOP-built request carries the + * process-local {@link markAgentLoopRequest} identity and arrives deep-frozen + * (mutation throws): its content is a pure function of the session log (the + * reconstructability Agent Note), so listeners read it, never rewrite it. + * Hand-built calls do not carry that marker; their messages already obey + * the immutable creation contract. + * @mode waterfall + */ +'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable): AsyncIterable +``` + +Types: [GenerateOptions](core.md) + +Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts) + diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index 7fd0043234..a10b696ec0 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -255,3 +255,191 @@ interface ContentBlockMap { ``` 块接口详见 [core.md § Content blocks and messages](core.md#content-blocks-and-messages)。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.llm` — `LlmService` + +The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall. + +```ts cordis-catalog +/** + * Register an adapter for the given provider routes. Throws `LlmError` with code + * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing). + * Disposed with the fiber. + * @param providers - every provider route this adapter should serve. + * @param adapter - the adapter that streams calls for those providers. + * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}. + */ +registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle + +/** + * Describe provider routes with a registered adapter. + * @returns detached provider metadata in registration order. + */ +listProviders(): LlmProviderInfo[] + +/** + * Declare provider routes an adapter plugin can activate through + * configuration. Registration is all-or-nothing: an empty list, invalid + * 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 a handle that withdraws all of them, and can atomically replace them. + */ +registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle + +/** + * List every declared configurable provider, registered or dormant. + * @returns detached directory entries in declaration order. + */ +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, ): () => 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 + +/** + * Resolve the retry policy captured when one provider route was registered. + * @param provider - registered provider route to inspect. + * @returns the provider-owned policy, with normal defaults already resolved. + */ +providerRetryPolicy(provider: string): ResolvedRetryPolicy + +/** + * Discover models advertised by one registered provider. Catalog membership + * is advisory and never changes routing or request validation. + * @param provider - registered provider route to inspect. + * @returns detached model metadata in adapter-preferred order. + */ +async listModels(provider: string): Promise + +/** + * Resolve and validate all metadata from the adapter that owns one exact + * route. The result is detached from adapter-owned objects; catalog + * membership remains advisory and does not control request routing. + * @param provider - registered provider route to inspect. + * @param model - exact model id passed to the adapter. + * @param signal - optional cancellation for adapter-owned asynchronous lookup. + * @returns exact model identity plus available context and reasoning metadata. + */ +async resolveModelInfo( provider: string, model: string, signal?: AbortSignal, ): Promise + +/** + * Validate a conversation call config against its exact model capability and + * materialize adapter-configured defaults. Unsupported explicit efforts + * reject before provider I/O; no clamping or aliasing is performed. This + * standalone query does not bind a later dispatch; use {@link prepareCall} + * when logging and streaming must share one adapter registration. + * @param config - provider/model route and optional request controls. + * @param signal - optional cancellation for adapter-owned capability lookup. + * @returns a detached config only when a default must be materialized. + */ +async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise + +/** + * Resolve one call under its current adapter registration. The returned + * one-shot handle keeps that registration across header logging and dispatch, + * so HMR cannot combine one adapter's capability result with another adapter. + * @param config - provider/model route and optional request controls. + * @param signal - optional cancellation for adapter-owned capability lookup. + * @returns a prepared config and its registration-bound stream entry point. + */ +async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise + +/** + * 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 +``` + +Types: [AdapterRegistrationHandle](core.md) · [DirectoryRegistrationHandle](core.md) · [GenerateOptions](core.md) · [LlmCallConfig](core.md) · [LlmConfigurableProvider](core.md) · [LlmDiscoveredModel](core.md) · [LlmModelDiscoveryRequest](core.md) · [LlmModelInfo](core.md) · [LlmProviderInfo](core.md) · [LlmResolvedModelInfo](core.md) + +Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts) + + + +### `llm/*` events + + + +#### `llm/adapters-updated` — emit + +The provider topology changed: an adapter registered or unregistered routes, or the configurable-provider directory gained or lost entries. This is a payload-free registry notification fired at each commit point (including registration disposal); consumers re-read `listProviders()`, `listModels()`, or `listConfigurableProviders()` for the new state. Observer failures are contained and cannot veto the registry mutation. + +```ts cordis-catalog +/** + * The provider topology changed: an adapter registered or unregistered + * routes, or the configurable-provider directory gained or lost entries. + * This is a payload-free registry notification fired at each commit point + * (including registration disposal); consumers re-read `listProviders()`, + * `listModels()`, or `listConfigurableProviders()` for the new state. + * Observer failures are contained and cannot veto the registry mutation. + * @mode emit + */ +'llm/adapters-updated'(): void +``` + +Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts) + + + +#### `llm/stream` — waterfall + +Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit. + +```ts cordis-catalog +/** + * Waterfall around every streaming model call (retry, replay, routing). + * Bound to the {@link LlmService}; call `next()` to reach the resolved + * adapter's stream, or yield your own chunks to short-circuit. + * @param options - the full request. A LOOP-built request carries the + * process-local {@link markAgentLoopRequest} identity and arrives deep-frozen + * (mutation throws): its content is a pure function of the session log (the + * reconstructability Agent Note), so listeners read it, never rewrite it. + * Hand-built calls do not carry that marker; their messages already obey + * the immutable creation contract. + * @mode waterfall + */ +'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable): AsyncIterable +``` + +Types: [GenerateOptions](core.md) + +Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts) + diff --git a/docs/subsystems/permission.i18n.yaml b/docs/subsystems/permission.i18n.yaml index c63da5efdc..6f6ba3b3c3 100644 --- a/docs/subsystems/permission.i18n.yaml +++ b/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: ec8cd4a923b617934f1612c31284103266aea4ef -permission.zh.md: 4534c33e2715e4a500344f547c7895128877f4fb +permission.md: 60ed88a79d0e51922f4cdffafa8b547669b49f5d +permission.zh.md: c06714fc6a111f56af680c3dea1764c90f09998d diff --git a/docs/subsystems/permission.md b/docs/subsystems/permission.md index ec8cd4a923..60ed88a79d 100644 --- a/docs/subsystems/permission.md +++ b/docs/subsystems/permission.md @@ -65,4 +65,67 @@ interface PresetOption { `set(session, name)` resolves the preset (unknown names throw), appends a log-only `permission/preset` event unless `name` is already the effective preset, then writes each knob through its own setter — `setSandboxMode` from [dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) and `setApprovalPolicy` from [dsh-user-approval](../../packages/interaction/user-approval) — only when that knob's effective value changes. The selection event precedes the knob events in the same turn, and re-selecting the effective preset appends nothing at all. -`permission/preset` is durable, log-only user intent: it stays out of the model transcript (the knob events own the model-visible consequences through their consumers), and it exists so `current()` can preserve WHICH preset the user chose when two presets share a bundle; `effectivePermissionPreset(events)` folds the last one, and replay needs no catch-up state. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md); the method signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxpermission--permissionservice). +`permission/preset` is durable, log-only user intent: it stays out of the model transcript (the knob events own the model-visible consequences through their consumers), and it exists so `current()` can preserve WHICH preset the user chose when two presets share a bundle; `effectivePermissionPreset(events)` folds the last one, and replay needs no catch-up state. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md); the method signatures are in the generated [service catalog](#ctxpermission--permissionservice). + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.permission` — `PermissionService` + +Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error. + +```ts cordis-catalog +/** + * Resolve the preset matching the effective knob values. A still-matching + * last selection wins shared-bundle ties; otherwise the first table match + * wins, or {@link CUSTOM_PRESET} when no entry matches. + * @param events - the session's events in log order. + * @returns the effective preset name, or `custom` when nothing matches. + */ +current(events: readonly SessionEvent[]): string + +/** + * Build the whole select value for one folded knob state: every table + * option in declaration order, `custom` appended exactly while derived. + * @param state - the folded knob overrides. + * @returns the `permissions` projection payload. + */ +selectFor(state: KnobState): PermissionSelect + +/** + * Resolve a preset's knob bundle. + * @param name - the preset name to resolve. + * @returns the configured bundle. + * @throws when `name` is not in the table. + */ +resolve(name: string): PresetSpec + +/** + * Build the client option for a table entry or {@link CUSTOM_PRESET}. A + * missing label falls back to the table key. + * @param name - a table key, or `custom`. + * @returns the option a client renders. + * @throws when `name` is neither a table key nor `custom`. + */ +optionOf(name: string): PresetOption + +/** + * Record a changed preset, then update each changed knob through its own + * setter. Selecting the effective preset again appends nothing. + * @param session - the session the switch belongs to. + * @param name - the preset to switch to; unknown names throw. + */ +set(session: Session, name: string): void +``` + +Types: [Session](session.md) · [SessionEvent](core.md) + +Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/src/index.ts) + diff --git a/docs/subsystems/permission.zh.md b/docs/subsystems/permission.zh.md index 4534c33e27..c06714fc6a 100644 --- a/docs/subsystems/permission.zh.md +++ b/docs/subsystems/permission.zh.md @@ -65,4 +65,67 @@ interface PresetOption { `set(session, name)` 解析预设(未知名称抛出异常),在 `name` 尚不是生效预设时追加一条仅记日志的 `permission/preset` 事件,然后通过各旋钮自己的 setter([dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) 的 `setSandboxMode` 与 [dsh-user-approval](../../packages/interaction/user-approval) 的 `setApprovalPolicy`)写入,且仅当该旋钮的生效值发生变化时才写。同一轮次内,选择事件先于旋钮事件出现;重新选择当前生效的预设则什么都不追加。 -`permission/preset` 是持久、仅记日志的用户意图:它不进入模型 transcript(文本记录),模型可见的后果由旋钮事件经各自消费方承担;它存在是为了在两个预设共享同一个旋钮组合时,让 `current()` 仍能保住用户选择的究竟是哪一个预设;`effectivePermissionPreset(events)` 折叠最后一条,回放不需要任何追赶状态。完整事件声明见[持久化日志事件目录](../persistence-catalog.md);方法签名见生成的[服务目录](../cordis-catalog/services.md#ctxpermission--permissionservice)。 +`permission/preset` 是持久、仅记日志的用户意图:它不进入模型 transcript(文本记录),模型可见的后果由旋钮事件经各自消费方承担;它存在是为了在两个预设共享同一个旋钮组合时,让 `current()` 仍能保住用户选择的究竟是哪一个预设;`effectivePermissionPreset(events)` 折叠最后一条,回放不需要任何追赶状态。完整事件声明见[持久化日志事件目录](../persistence-catalog.md);方法签名见生成的[服务目录](#ctxpermission--permissionservice)。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.permission` — `PermissionService` + +Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error. + +```ts cordis-catalog +/** + * Resolve the preset matching the effective knob values. A still-matching + * last selection wins shared-bundle ties; otherwise the first table match + * wins, or {@link CUSTOM_PRESET} when no entry matches. + * @param events - the session's events in log order. + * @returns the effective preset name, or `custom` when nothing matches. + */ +current(events: readonly SessionEvent[]): string + +/** + * Build the whole select value for one folded knob state: every table + * option in declaration order, `custom` appended exactly while derived. + * @param state - the folded knob overrides. + * @returns the `permissions` projection payload. + */ +selectFor(state: KnobState): PermissionSelect + +/** + * Resolve a preset's knob bundle. + * @param name - the preset name to resolve. + * @returns the configured bundle. + * @throws when `name` is not in the table. + */ +resolve(name: string): PresetSpec + +/** + * Build the client option for a table entry or {@link CUSTOM_PRESET}. A + * missing label falls back to the table key. + * @param name - a table key, or `custom`. + * @returns the option a client renders. + * @throws when `name` is neither a table key nor `custom`. + */ +optionOf(name: string): PresetOption + +/** + * Record a changed preset, then update each changed knob through its own + * setter. Selecting the effective preset again appends nothing. + * @param session - the session the switch belongs to. + * @param name - the preset to switch to; unknown names throw. + */ +set(session: Session, name: string): void +``` + +Types: [Session](session.md) · [SessionEvent](core.md) + +Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/src/index.ts) + diff --git a/docs/subsystems/persistence.i18n.yaml b/docs/subsystems/persistence.i18n.yaml index 697094b490..808abb402a 100644 --- a/docs/subsystems/persistence.i18n.yaml +++ b/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: 177d596d3d01134266f139b47430e1e5064d04d6 -persistence.zh.md: 6582c0d7d02d1976d5c3ca3322284098130ed4ad +persistence.md: 1edf3c88fbabe55058bbf94ed8f31decf47cb84a +persistence.zh.md: dbebd9ff819c8d50cfa9748678dac09c86adde6d diff --git a/docs/subsystems/persistence.md b/docs/subsystems/persistence.md index 177d596d3d..1edf3c88fb 100644 --- a/docs/subsystems/persistence.md +++ b/docs/subsystems/persistence.md @@ -204,3 +204,135 @@ Both implement the same abstract `SessionPersistence` (locate/create/append/prep - **[dsh-session-persistence-jsonl](../../packages/session/session-persistence-jsonl)** — an append-only logical JSONL log per session, stored as checksummed concatenated Zstandard frames by default or raw lines by configuration, with crash-safe atomic writes, interrupted-turn recovery, and a read/replay path. - **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)** — `node:sqlite`, one row per `SessionEvent`. The row shape `(session_id, seq, type, time, data, source_event_seqs, surface_op)` maps 1:1 onto the event, including optional surface metadata, so there is no parallel persisted schema to keep in sync. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionPersistence` — `SessionPersistence` (abstract seam) + +Durable append-only session storage. Implementations preserve contiguous, losslessly JSON-serializable events; append resolves only after durability, and load balances a complete interrupted tail without rewriting committed events. + +```ts cordis-catalog +/** + * Resolve this backend's independent local artifact for a session without + * reading, creating, flushing, or otherwise materializing it. Backends such + * as SQLite that do not own one artifact per session return `undefined`. + * @param meta - the immutable session header whose artifact is requested. + * @returns the backend-specific absolute location, when one exists. + */ +abstract locate(meta: SessionHeader): SessionLocation | undefined + +/** + * Register a new session's metadata. A backend MAY defer the physical write + * until the first {@link append} (lazy materialization), in which case a + * created-but-never-appended session is absent from {@link list} + * — abandoned sessions leave nothing behind. + * @param meta - the immutable header (id, version, cwd, lineage) to record. + */ +abstract create(meta: SessionHeader): Promise + +/** + * Durably persist a batch of events. Honors the append-only and contiguous- + * seq contracts: the first event's `seq` MUST equal the stored next-seq + * (after `load` has durably closed any interrupted turn). Rejects non-JSON- + * serializable `event.data` with an error naming the offending event type. + * @param id - the session the batch belongs to. + * @param events - the contiguous batch to persist, in seq order. + */ +abstract append(id: SessionId, events: readonly SessionEvent[]): Promise + +/** + * Prepare the exact unpublished Session used by resume. Implementations may + * reuse object graphs retained by an earlier {@link inspect} after confirming + * their durable revision is still current; disposal releases an unpublished + * reservation. Revision retries require the durable log to remain unchanged + * for one read/check round trip; continuous external writers may delay completion. + * @param id - persisted session to prepare. + * @param signal - optional cancellation for preparation work. + * @returns one owned unpublished Session preparation. + */ +async prepare(id: SessionId, signal?: AbortSignal): Promise + +/** + * Load an immutable balanced logical view and commit any required cold + * recovery. A complete interrupted final turn is preserved and durably + * closed with missing tool errors plus any open step and turn boundaries; + * only a torn final record is discarded. Unknown versions and corruption in + * the committed prefix reject. Implementations MUST NOT crash-repair an + * identity still bound to a live Session: a balanced live log may return as a + * durable snapshot, while an open live turn rejects. Returned values may be + * shared with immutable live or prepared state and must not be mutated. + * Revision-based implementations may wait for one stable read/check round trip. + * @param id - the persisted session to reload. + * @returns the header and a log ending on a balanced `turn/end`. + */ +abstract load(id: SessionId): Promise + +/** + * Inspect an immutable logical session without committing recovery or + * publishing it. A cold complete interrupted turn receives synthetic closers + * in memory and a torn physical tail remains untouched. An already-live + * Session instead yields its current immutable snapshot, which may contain an + * open turn and its `session/end-seed` boundary. Coordinator-backed + * implementations retain the exact cold unpublished Session for bounded + * reuse by a later {@link prepare}. A stale ready source is reloaded; a source + * already committing or reserved for resume remains exclusive, and inspection + * may borrow its immutable view. Callers borrow only the immutable header and + * log. Continuous external writers may delay revision convergence. + * @param id - the persisted session to inspect. + * @param signal - optional cancellation for queued and backend read work. + * @returns the validated header and current logical event log. + */ +abstract inspect(id: SessionId, signal?: AbortSignal): Promise + +/** + * Read the stored events from `fromSeq` onward — the read-from-seq + * primitive for read models that resume from a watermark (e.g. a persisted + * projection cache folding only the tail past its checkpoint). Unlike + * {@link inspect}, it is a detached physical suffix read: no preparation + * cache, torn-tail truncation, synthetic closers, or coordinator-state + * publication. Only events from the valid contiguous stored prefix are + * returned, so a torn fragment never reaches the caller. `fromSeq` at or + * beyond the stored prefix returns an empty event list (never an error). + * Backends whose medium can seek by seq + * (SQLite) read only the suffix; sequential media (JSONL, both encodings) + * still parse the whole artifact and skip forward — the primitive bounds + * what is RETURNED and refolded, not every backend's physical read. + * @param id - the persisted session to read. + * @param fromSeq - first event seq to include; a non-negative safe integer. + * @param signal - optional cancellation for queued and backend read work. + * @returns the header and the stored events with `seq >= fromSeq`. + */ +abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }> + +/** + * Lightweight listing from metadata, without a full-log parse. + * @param signal - optional cancellation for backend listing work. + * @returns one header per materialized session. + */ +abstract list(signal?: AbortSignal): Promise + +/** + * List materialized sessions with cheap per-log change tokens. + * + * Repeated observations of an unchanged log return the same revision. A + * successful mutating {@link load} repair changes the next listed revision. + * Revisions also distinguish independently backed stores so backend-local + * counters cannot compare equal across different persistence sources. + * @param signal - optional cancellation for backend snapshot-listing work. + * @returns one header and opaque revision per materialized session without loading full logs. + */ +abstract listSnapshots(signal?: AbortSignal): Promise +``` + +Types: [SessionEvent](core.md) · [SessionId](core.md) + +Source: [`packages/session/session-persistence/src/index.ts:72`](../../packages/session/session-persistence/src/index.ts) + diff --git a/docs/subsystems/persistence.zh.md b/docs/subsystems/persistence.zh.md index 6582c0d7d0..dbebd9ff81 100644 --- a/docs/subsystems/persistence.zh.md +++ b/docs/subsystems/persistence.zh.md @@ -204,3 +204,135 @@ interface SessionPersistenceSnapshot { - **[dsh-session-persistence-jsonl](../../packages/session/session-persistence-jsonl)**——每个会话一份仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame,也可配置为原始行;支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。 - **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行结构 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionPersistence` — `SessionPersistence` (abstract seam) + +Durable append-only session storage. Implementations preserve contiguous, losslessly JSON-serializable events; append resolves only after durability, and load balances a complete interrupted tail without rewriting committed events. + +```ts cordis-catalog +/** + * Resolve this backend's independent local artifact for a session without + * reading, creating, flushing, or otherwise materializing it. Backends such + * as SQLite that do not own one artifact per session return `undefined`. + * @param meta - the immutable session header whose artifact is requested. + * @returns the backend-specific absolute location, when one exists. + */ +abstract locate(meta: SessionHeader): SessionLocation | undefined + +/** + * Register a new session's metadata. A backend MAY defer the physical write + * until the first {@link append} (lazy materialization), in which case a + * created-but-never-appended session is absent from {@link list} + * — abandoned sessions leave nothing behind. + * @param meta - the immutable header (id, version, cwd, lineage) to record. + */ +abstract create(meta: SessionHeader): Promise + +/** + * Durably persist a batch of events. Honors the append-only and contiguous- + * seq contracts: the first event's `seq` MUST equal the stored next-seq + * (after `load` has durably closed any interrupted turn). Rejects non-JSON- + * serializable `event.data` with an error naming the offending event type. + * @param id - the session the batch belongs to. + * @param events - the contiguous batch to persist, in seq order. + */ +abstract append(id: SessionId, events: readonly SessionEvent[]): Promise + +/** + * Prepare the exact unpublished Session used by resume. Implementations may + * reuse object graphs retained by an earlier {@link inspect} after confirming + * their durable revision is still current; disposal releases an unpublished + * reservation. Revision retries require the durable log to remain unchanged + * for one read/check round trip; continuous external writers may delay completion. + * @param id - persisted session to prepare. + * @param signal - optional cancellation for preparation work. + * @returns one owned unpublished Session preparation. + */ +async prepare(id: SessionId, signal?: AbortSignal): Promise + +/** + * Load an immutable balanced logical view and commit any required cold + * recovery. A complete interrupted final turn is preserved and durably + * closed with missing tool errors plus any open step and turn boundaries; + * only a torn final record is discarded. Unknown versions and corruption in + * the committed prefix reject. Implementations MUST NOT crash-repair an + * identity still bound to a live Session: a balanced live log may return as a + * durable snapshot, while an open live turn rejects. Returned values may be + * shared with immutable live or prepared state and must not be mutated. + * Revision-based implementations may wait for one stable read/check round trip. + * @param id - the persisted session to reload. + * @returns the header and a log ending on a balanced `turn/end`. + */ +abstract load(id: SessionId): Promise + +/** + * Inspect an immutable logical session without committing recovery or + * publishing it. A cold complete interrupted turn receives synthetic closers + * in memory and a torn physical tail remains untouched. An already-live + * Session instead yields its current immutable snapshot, which may contain an + * open turn and its `session/end-seed` boundary. Coordinator-backed + * implementations retain the exact cold unpublished Session for bounded + * reuse by a later {@link prepare}. A stale ready source is reloaded; a source + * already committing or reserved for resume remains exclusive, and inspection + * may borrow its immutable view. Callers borrow only the immutable header and + * log. Continuous external writers may delay revision convergence. + * @param id - the persisted session to inspect. + * @param signal - optional cancellation for queued and backend read work. + * @returns the validated header and current logical event log. + */ +abstract inspect(id: SessionId, signal?: AbortSignal): Promise + +/** + * Read the stored events from `fromSeq` onward — the read-from-seq + * primitive for read models that resume from a watermark (e.g. a persisted + * projection cache folding only the tail past its checkpoint). Unlike + * {@link inspect}, it is a detached physical suffix read: no preparation + * cache, torn-tail truncation, synthetic closers, or coordinator-state + * publication. Only events from the valid contiguous stored prefix are + * returned, so a torn fragment never reaches the caller. `fromSeq` at or + * beyond the stored prefix returns an empty event list (never an error). + * Backends whose medium can seek by seq + * (SQLite) read only the suffix; sequential media (JSONL, both encodings) + * still parse the whole artifact and skip forward — the primitive bounds + * what is RETURNED and refolded, not every backend's physical read. + * @param id - the persisted session to read. + * @param fromSeq - first event seq to include; a non-negative safe integer. + * @param signal - optional cancellation for queued and backend read work. + * @returns the header and the stored events with `seq >= fromSeq`. + */ +abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }> + +/** + * Lightweight listing from metadata, without a full-log parse. + * @param signal - optional cancellation for backend listing work. + * @returns one header per materialized session. + */ +abstract list(signal?: AbortSignal): Promise + +/** + * List materialized sessions with cheap per-log change tokens. + * + * Repeated observations of an unchanged log return the same revision. A + * successful mutating {@link load} repair changes the next listed revision. + * Revisions also distinguish independently backed stores so backend-local + * counters cannot compare equal across different persistence sources. + * @param signal - optional cancellation for backend snapshot-listing work. + * @returns one header and opaque revision per materialized session without loading full logs. + */ +abstract listSnapshots(signal?: AbortSignal): Promise +``` + +Types: [SessionEvent](core.md) · [SessionId](core.md) + +Source: [`packages/session/session-persistence/src/index.ts:72`](../../packages/session/session-persistence/src/index.ts) + diff --git a/docs/subsystems/plan.i18n.yaml b/docs/subsystems/plan.i18n.yaml index f82002420b..efe9e376c6 100644 --- a/docs/subsystems/plan.i18n.yaml +++ b/docs/subsystems/plan.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/plan.md -plan.md: 661a393b18f9154771c0b9358ad7f016a4fdec17 -plan.zh.md: 068b63fd9f3bf46b7962031bba154cc7fc7daafd +plan.md: ee4b911050863765960ba46952ba923a7115e129 +plan.zh.md: 0481bdf76adf2f00fe18073fe8daa5ba172bbfe1 diff --git a/docs/subsystems/plan.md b/docs/subsystems/plan.md index 661a393b18..ee4b911050 100644 --- a/docs/subsystems/plan.md +++ b/docs/subsystems/plan.md @@ -36,4 +36,51 @@ When [`ctx.commands`](commands.md) is composed, the plugin registers `/plan [off ## The service -`ctx.planMode` owns the logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool; `get`/`set` signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxplanmode--planmodeservice). +`ctx.planMode` owns the logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool; `get`/`set` signatures are in the generated [service catalog](#ctxplanmode--planmodeservice). + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.planMode` — `PlanModeService` + +`ctx.planMode`: owns logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool. UIs observe committed flips through `session/event`; there is no live mirror. + +```ts cordis-catalog +/** + * Read the logged plan state and any selected state awaiting a boundary. + * + * @param agent The agent to read. + * @returns Current logged state plus a pending selection, when present. + */ +get(agent: Agent): { active: boolean; pending?: boolean } + +/** + * Select whether plan mode should be active. Between turns the change + * commits immediately — no request boundary would arrive until the next + * prompt, so a queued intent would hang (the open-turn fold is the idle + * signal: agent status stays `running` through post-turn checkpointing, + * where a boundary equally never comes). During an open turn the + * selection is held as pending intent for the next in-turn request + * boundary. Repeated selection of the current or already-pending state is + * a no-op. + * + * @param agent The agent to switch. + * @param active Whether plan mode should be active. + * @returns what happened: `committed` (logged now), `queued` (awaiting the + * next boundary), `cancelled` (an opposite pending selection was cleared; + * the logged state already matches), or `noop` (already in that state). + */ +set(agent: Agent, active: boolean): 'committed' | 'queued' | 'cancelled' | 'noop' +``` + +Types: [Agent](core.md) + +Source: [`packages/plan/plan-mode/src/index.ts:183`](../../packages/plan/plan-mode/src/index.ts) + diff --git a/docs/subsystems/plan.zh.md b/docs/subsystems/plan.zh.md index 068b63fd9f..0481bdf76a 100644 --- a/docs/subsystems/plan.zh.md +++ b/docs/subsystems/plan.zh.md @@ -36,4 +36,51 @@ interface PlanModeConfig { ## 服务 -`ctx.planMode` 拥有已记录的计划状态、边界处的应用与叙述、`plan:policy` 段落、`/plan` 命令和稳定注册的退出工具;`get`/`set` 签名见生成的[服务目录](../cordis-catalog/services.md#ctxplanmode--planmodeservice)。 +`ctx.planMode` 拥有已记录的计划状态、边界处的应用与叙述、`plan:policy` 段落、`/plan` 命令和稳定注册的退出工具;`get`/`set` 签名见生成的[服务目录](#ctxplanmode--planmodeservice)。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.planMode` — `PlanModeService` + +`ctx.planMode`: owns logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool. UIs observe committed flips through `session/event`; there is no live mirror. + +```ts cordis-catalog +/** + * Read the logged plan state and any selected state awaiting a boundary. + * + * @param agent The agent to read. + * @returns Current logged state plus a pending selection, when present. + */ +get(agent: Agent): { active: boolean; pending?: boolean } + +/** + * Select whether plan mode should be active. Between turns the change + * commits immediately — no request boundary would arrive until the next + * prompt, so a queued intent would hang (the open-turn fold is the idle + * signal: agent status stays `running` through post-turn checkpointing, + * where a boundary equally never comes). During an open turn the + * selection is held as pending intent for the next in-turn request + * boundary. Repeated selection of the current or already-pending state is + * a no-op. + * + * @param agent The agent to switch. + * @param active Whether plan mode should be active. + * @returns what happened: `committed` (logged now), `queued` (awaiting the + * next boundary), `cancelled` (an opposite pending selection was cleared; + * the logged state already matches), or `noop` (already in that state). + */ +set(agent: Agent, active: boolean): 'committed' | 'queued' | 'cancelled' | 'noop' +``` + +Types: [Agent](core.md) + +Source: [`packages/plan/plan-mode/src/index.ts:183`](../../packages/plan/plan-mode/src/index.ts) + diff --git a/docs/subsystems/pty.i18n.yaml b/docs/subsystems/pty.i18n.yaml index 11788e8e76..76b4500aea 100644 --- a/docs/subsystems/pty.i18n.yaml +++ b/docs/subsystems/pty.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/pty.md -pty.md: 97e1e662d1128ab0555e34f8284cf69d7d9d0d1a -pty.zh.md: a57f7448274de583dbb110ba9499e15dfb4de5f9 +# pnpm run verify-translation-pairing --write docs/subsystems/pty.md +pty.md: 45b157c73926d85d5fc2bb037d3954da7bf75063 +pty.zh.md: 4409ef0b0504780b0b5564a34b61d4f1b2c02775 diff --git a/docs/subsystems/pty.md b/docs/subsystems/pty.md index 97e1e662d1..45b157c739 100644 --- a/docs/subsystems/pty.md +++ b/docs/subsystems/pty.md @@ -89,3 +89,96 @@ interface PtySendResult { ## Ownership and durability `PtyService` attaches one awaited cleanup to the exact owner scope, rejects foreign operations, and keeps sessions alive across backend or tool-plugin reload. PTY state and raw bytes remain process-local. Model input and bounded returned output are durable through the existing `tool/call`, `tool/result`, and task-result paths rather than duplicate PTY session events. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.pty` — `PtyService` + +In-process registry for replaceable PTY backends and exact-Agent sessions. + +```ts cordis-catalog +/** + * Register one backend type for this effect scope. + * @param backend - provider with a non-empty unique type. + * @returns disposer that removes exactly this contribution. + */ +registerBackend(backend: PtyBackend): () => void + +/** + * List registered backend types in registration order. + * @returns fresh backend type names. + */ +listBackends(): string[] + +/** + * Create and publish one owner-scoped session after backend setup succeeds. + * @param owner - exact registered Agent that owns access and cleanup. + * @param request - backend type plus optional owner-local name and cwd. + * @param signal - cancellation of unpublished setup. + * @returns published identity, metadata, status, and MOTD. + */ +async spawn(owner: Agent, request: PtySpawnRequest, signal?: AbortSignal): Promise + +/** + * Test whether an exact owner has a published session or unpublished spawn. + * @param owner - exact live owner to inspect. + * @returns true across the entire spawn-to-close interval, with no publication gap. + */ +hasOwnerActivity(owner: Agent): boolean + +/** + * Start one exclusive interactive send. + * @param owner - exact session owner. + * @param id - target PTY identity. + * @param request - explicit text, submit behavior, and cancellation. + * @returns live operation handle for foreground await or task registration. + */ +startSend(owner: Agent, id: PtySessionId, request: PtySendRequest): PtySendOperation + +/** + * Read one bounded scrollback page from an owned session. + * @param owner - exact session owner. + * @param id - target PTY identity. + * @param request - optional newest-relative offset and line count. + * @returns bounded retained text and pagination metadata. + */ +read(owner: Agent, id: PtySessionId, request: PtyReadRequest = {}): PtyReadResult + +/** + * Deliver an allowed signal through an owned backend session. + * @param owner - exact session owner. + * @param id - target PTY identity. + * @param signal - allowed POSIX signal name. + * @returns delivered foreground process-group identity. + */ +signal(owner: Agent, id: PtySessionId, signal: PtySignal): Promise + +/** + * Close one owned session and remove it only after quiescent backend cleanup. + * @param owner - exact session owner. + * @param id - target PTY identity. + * @param reason - diagnostic cleanup reason. + * @returns true for a newly closed session, false when the same close is already in flight. + */ +async kill(owner: Agent, id: PtySessionId, reason: string = 'model request'): Promise + +/** + * List fresh snapshots for exactly one owner. + * @param owner - exact owner whose sessions are visible. + * @returns owner-visible snapshots in publication order. + */ +list(owner: Agent): PtySessionSnapshot[] +``` + +Types: [Agent](core.md) + +Source: [`packages/pty/pty/src/index.ts:105`](../../packages/pty/pty/src/index.ts) + diff --git a/docs/subsystems/pty.zh.md b/docs/subsystems/pty.zh.md index a57f744827..4409ef0b05 100644 --- a/docs/subsystems/pty.zh.md +++ b/docs/subsystems/pty.zh.md @@ -89,3 +89,96 @@ interface PtySendResult { ## 归属与持久性 `PtyService` 会将一项等待完成的清理附加到确切的拥有者作用域,拒绝其他拥有者的操作,并让会话在后端或工具插件重载期间保持存活。PTY 状态与原始字节仍局限在进程内。模型输入与有界返回输出通过现有 `tool/call`、`tool/result` 和任务结果路径持久保存,而不是重复记录 PTY 会话事件。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.pty` — `PtyService` + +In-process registry for replaceable PTY backends and exact-Agent sessions. + +```ts cordis-catalog +/** + * Register one backend type for this effect scope. + * @param backend - provider with a non-empty unique type. + * @returns disposer that removes exactly this contribution. + */ +registerBackend(backend: PtyBackend): () => void + +/** + * List registered backend types in registration order. + * @returns fresh backend type names. + */ +listBackends(): string[] + +/** + * Create and publish one owner-scoped session after backend setup succeeds. + * @param owner - exact registered Agent that owns access and cleanup. + * @param request - backend type plus optional owner-local name and cwd. + * @param signal - cancellation of unpublished setup. + * @returns published identity, metadata, status, and MOTD. + */ +async spawn(owner: Agent, request: PtySpawnRequest, signal?: AbortSignal): Promise + +/** + * Test whether an exact owner has a published session or unpublished spawn. + * @param owner - exact live owner to inspect. + * @returns true across the entire spawn-to-close interval, with no publication gap. + */ +hasOwnerActivity(owner: Agent): boolean + +/** + * Start one exclusive interactive send. + * @param owner - exact session owner. + * @param id - target PTY identity. + * @param request - explicit text, submit behavior, and cancellation. + * @returns live operation handle for foreground await or task registration. + */ +startSend(owner: Agent, id: PtySessionId, request: PtySendRequest): PtySendOperation + +/** + * Read one bounded scrollback page from an owned session. + * @param owner - exact session owner. + * @param id - target PTY identity. + * @param request - optional newest-relative offset and line count. + * @returns bounded retained text and pagination metadata. + */ +read(owner: Agent, id: PtySessionId, request: PtyReadRequest = {}): PtyReadResult + +/** + * Deliver an allowed signal through an owned backend session. + * @param owner - exact session owner. + * @param id - target PTY identity. + * @param signal - allowed POSIX signal name. + * @returns delivered foreground process-group identity. + */ +signal(owner: Agent, id: PtySessionId, signal: PtySignal): Promise + +/** + * Close one owned session and remove it only after quiescent backend cleanup. + * @param owner - exact session owner. + * @param id - target PTY identity. + * @param reason - diagnostic cleanup reason. + * @returns true for a newly closed session, false when the same close is already in flight. + */ +async kill(owner: Agent, id: PtySessionId, reason: string = 'model request'): Promise + +/** + * List fresh snapshots for exactly one owner. + * @param owner - exact owner whose sessions are visible. + * @returns owner-visible snapshots in publication order. + */ +list(owner: Agent): PtySessionSnapshot[] +``` + +Types: [Agent](core.md) + +Source: [`packages/pty/pty/src/index.ts:105`](../../packages/pty/pty/src/index.ts) + diff --git a/docs/subsystems/sandbox.i18n.yaml b/docs/subsystems/sandbox.i18n.yaml index 34691ad25b..efbd158e18 100644 --- a/docs/subsystems/sandbox.i18n.yaml +++ b/docs/subsystems/sandbox.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/sandbox.md -sandbox.md: 9e5feafe046f18dad49aeaf281793f0b8e03c240 -sandbox.zh.md: a1314d1b78eb0d46ea4c8ca5aa330ee83bf89132 +# pnpm run verify-translation-pairing --write docs/subsystems/sandbox.md +sandbox.md: dd960b3021dcdc87cfd36fd439cbec0a810dd736 +sandbox.zh.md: f526982a782144a0bf58abcde5d883e9dc813369 diff --git a/docs/subsystems/sandbox.md b/docs/subsystems/sandbox.md index 9e5feafe04..dd960b3021 100644 --- a/docs/subsystems/sandbox.md +++ b/docs/subsystems/sandbox.md @@ -146,3 +146,65 @@ The [local provider](../../packages/sandbox/sandbox-local/README.md) owns operat `ctx.sandbox.confine(argv, policy)` returns a `ConfinedArgv` or throws `SandboxUnavailableError` with code `SANDBOX_UNAVAILABLE` when no usable backend exists. Consumers may also classify a failure while spawning or observing the returned argv; that attribution belongs to the consumer contract. Silent unconfined passthrough is never legal for a confined policy. Provider selection, probing, caching, and backend-specific enforcement reports belong to the [local provider](../../packages/sandbox/sandbox-local/README.md). + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sandbox` — `SandboxProvider` (abstract seam) + +Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end. + +```ts cordis-catalog +/** + * Wrap `argv` so it executes confined under `policy` on this host; the + * caller spawns the returned argv in place of its own. + * @param argv - the exact argv the caller is about to spawn (program plus + * arguments), NOT a shell string — a shell-shaped consumer passes + * `['bash', '-c', command]`. + * @param policy - the file-effect policy this execution runs under, + * carried per call (see {@link SandboxPolicy}). + * @returns the argv to spawn instead, plus the enforcement completeness + * the selected backend achieves for it. + */ +abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv +``` + +Source: [`packages/sandbox/sandbox/src/index.ts:148`](../../packages/sandbox/sandbox/src/index.ts) + + + +### `ctx.sandboxPolicy` — `SandboxPolicyService` + +The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability. + +```ts cordis-catalog +/** + * Resolve the complete policy for one capability call. An approved explicit + * mode outranks the session's last `sandbox/mode` event, which outranks the + * deployment default. A session cwd is its workspace-write boundary; the + * configured root is the fallback for agentless calls and sessions without a + * cwd. + * @param request - optional session and approved mode override. + * @returns the fully resolved per-call mode and absolute workspace root. + */ +resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy + +/** + * Read the session override without applying the deployment default. + * @param session - session whose log supplies the override. + * @returns the last logged mode, or `undefined` without one. + */ +overrideOf(session: Session): SandboxMode | undefined +``` + +Types: [Session](session.md) + +Source: [`packages/sandbox/sandbox-policy/src/index.ts:91`](../../packages/sandbox/sandbox-policy/src/index.ts) + diff --git a/docs/subsystems/sandbox.zh.md b/docs/subsystems/sandbox.zh.md index a1314d1b78..f526982a78 100644 --- a/docs/subsystems/sandbox.zh.md +++ b/docs/subsystems/sandbox.zh.md @@ -146,3 +146,65 @@ interface ConfinedArgv { `ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。消费方也可以在 spawn 或观察所返回的 argv 时对失败进行分类;该归因属于消费方契约。对于受限策略,静默的无隔离透传永远不合法。 提供方选择、探测、缓存和后端专有的强制执行报告归[本地提供方](../../packages/sandbox/sandbox-local/README.md)所有。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sandbox` — `SandboxProvider` (abstract seam) + +Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end. + +```ts cordis-catalog +/** + * Wrap `argv` so it executes confined under `policy` on this host; the + * caller spawns the returned argv in place of its own. + * @param argv - the exact argv the caller is about to spawn (program plus + * arguments), NOT a shell string — a shell-shaped consumer passes + * `['bash', '-c', command]`. + * @param policy - the file-effect policy this execution runs under, + * carried per call (see {@link SandboxPolicy}). + * @returns the argv to spawn instead, plus the enforcement completeness + * the selected backend achieves for it. + */ +abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv +``` + +Source: [`packages/sandbox/sandbox/src/index.ts:148`](../../packages/sandbox/sandbox/src/index.ts) + + + +### `ctx.sandboxPolicy` — `SandboxPolicyService` + +The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability. + +```ts cordis-catalog +/** + * Resolve the complete policy for one capability call. An approved explicit + * mode outranks the session's last `sandbox/mode` event, which outranks the + * deployment default. A session cwd is its workspace-write boundary; the + * configured root is the fallback for agentless calls and sessions without a + * cwd. + * @param request - optional session and approved mode override. + * @returns the fully resolved per-call mode and absolute workspace root. + */ +resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy + +/** + * Read the session override without applying the deployment default. + * @param session - session whose log supplies the override. + * @returns the last logged mode, or `undefined` without one. + */ +overrideOf(session: Session): SandboxMode | undefined +``` + +Types: [Session](session.md) + +Source: [`packages/sandbox/sandbox-policy/src/index.ts:91`](../../packages/sandbox/sandbox-policy/src/index.ts) + diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml new file mode 100644 index 0000000000..24b1c7b234 --- /dev/null +++ b/docs/subsystems/session-projection.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 docs/subsystems/session-projection.md +session-projection.md: 56f9fb43dddc482264aab52ceeb9b9920df148cd +session-projection.zh.md: 4a2310fed4af6f018af07b5f2b46d3c28aa361a4 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md new file mode 100644 index 0000000000..56f9fb43dd --- /dev/null +++ b/docs/subsystems/session-projection.md @@ -0,0 +1,262 @@ +# Session Projections + +English | [中文](session-projection.zh.md) + +The session-projection seam — a [capability seam](../capability-seams.md) through which domain host plugins serve whole current values of log-derived per-session state to client carriers: the interface and registry ([dsh-session-projection](../../packages/session/session-projection), `ctx.sessionProjections`), domain contributors (each registering one pure unit), and carriers ([dsh-host-apiproxy](../../packages/host/apiproxy)'s history tail page and `session/projection` push frame today). It is one optional capability, not part of the agent-loop spine. The framework drives, the domain computes: the registry subscribes to `session/event` once and folds every committed event through every unit; domains hold no subscriptions and clients never fold domain events — they receive finished values. Design authority: the [session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md); drive/cache/feed contracts: the [package README](../../packages/session/session-projection/README.md). + +Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts) + +## The unit + +`SessionProjectionMap` is the merge-extensible type table for the whole chain (host unit, wire block, client hook); values are wire-JSON whole values, and rendering belongs to the slot system, never this layer. A domain contributes one `ProjectionDefinition` per key: + +```ts type-equiv +/** + * One domain's state-driven computation unit: three pure synchronous + * functions plus declarations — never an opaque getter. The framework drives + * `apply` on every committed session event; the domain holds no + * subscriptions and owns only the mathematics. All three functions MUST be + * synchronous (an async unit would tear the carriers' consistency cut) and + * `state` MUST be plain JSON (the persisted-cache precondition). + */ +interface ProjectionDefinition { + /** The projection key this unit owns (its `SessionProjectionMap` entry). */ + key: K + /** Validates the wire payload (`view` output) before it leaves the host. */ + schema: ZodType + /** + * State for the empty log. + * @returns the initial state. + */ + init(): S + /** + * Pure transition: previous state + one committed event → next state. A + * unit uninterested in an event MUST return the same state reference — an + * unchanged reference (`Object.is`) produces zero downstream work. + * @param state - the state covering all prior events. + * @param event - the next committed session event. + * @returns the next state (same reference when the event is not the unit's). + */ + apply(state: S, event: SessionEvent): S + /** + * State → wire payload (the read-side projection). + * @param state - the current state. + * @returns the whole current value for this unit's key. + */ + view(state: S): SessionProjectionMap[K] + /** + * Persisted-cache invalidation anchor: bump whenever the state shape or the + * fold semantics change, so persisted `(sessionId, key, ver, seq, val)` + * rows from an older unit are discarded instead of being forward-applied + * into garbage. Non-negative integer. + */ + stateVersion: number +} +``` + +The whole-value event rule is load-bearing: a state-carrying log event carries the complete post-change state, never a bare delta — it keeps every transition trivially cheap and every served value self-describing (last-wins for consumers). + +## The snapshot and the change feed + +```ts type-equiv +/** + * One consistent read cut over every registered unit for one session. + * `asOfSeq` is the shared watermark — the seq of the last event every value + * reflects (`-1` for an empty log, mirroring `session/subscribed.lastSeq`). + */ +interface ProjectionSnapshot { + /** Seq of the last event the values reflect; -1 for an empty log. */ + asOfSeq: number + /** Whole current value per registered key. */ + values: Partial +} +``` + +```ts type-equiv +/** + * Change-feed listener: one unit's value changed for one session. `value` is + * the schema-validated `view` output; `seq` is the unit's watermark at + * emission (the seq of the event that caused the change). + */ +type ProjectionChangeListener = ( + session: Session, + key: Extract, + value: unknown, + seq: number, +) => void +``` + +`snapshot(session)` is fully synchronous — a carrier reads it in the same tick as its page slice, which is what makes `asOfSeq` one consistent cut — and every value passes its unit's schema before leaving (an accidentally-async `view` returns a Promise, which fails that boundary parse loudly). The change feed fires once per unit whose state *reference* changed, per committed event: the same-reference discipline in `apply` is the gate. + +## The registry: `ctx.sessionProjections` + +`SessionProjectionRegistry` ([signatures](#ctxsessionprojections--sessionprojectionregistry)) owns the drive: one `session/event` subscription, eager `apply` over every registered unit, and per-session per-unit watermark cells. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect whose disposer rides the calling fiber: an unloaded domain plugin's key (with its cached cells) disappears from subsequent drives and snapshots, and clients read that as capability absence; duplicate keys throw. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionProjectionCache` — `SessionProjectionCache` + +The persisted projection cache service. Opens the `session_projcache` domain at init, checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus two mandatory points — `turn/end` and session disposal (the live-to-cold moment) — and serves the cold-read ladder: cached row, persistence `readFrom` tail, registry `restore`, durable write-back. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write or cold read. + +```ts cordis-catalog +/** + * The zero-I/O listing read: whole values viewed straight from the stored + * rows (version-matching keys only), each cut carried with its watermark + * so a client value store can seed under its higher-seq-wins rule — as + * stale as the last durable checkpoint but never wrong, and never from an + * unrelated log (the caller's header is the identity witness). Fresher + * paths (the history tail baseline, {@link coldSnapshot}) supersede these + * values whenever a session is actually opened. + * @param meta - the listed session's header (identity witness; no log read). + * @returns the cut (`asOfSeq` = lowest served-row watermark), or + * `undefined` when no usable row exists for this lifecycle. + */ +cachedSnapshot(meta: SessionHeader): ProjectionSnapshot | undefined + +/** + * Durably checkpoint one live session NOW (both mandatory points call + * this; tests and carriers may too). The registry cut is snapshotted at + * this boundary (states are live references), then the whole record is + * replaced. NOT fail-soft — callers on the fail-soft paths contain it. + * @param session - the live session to checkpoint. + * @returns resolution after durability and event emission. + */ +async write(session: Session): Promise + +/** + * Cold-read one persisted session's projections with zero full-log load: + * cached rows + a persistence `readFrom` tail from the registry's restore + * floor, refolded by the registry and written back (fail-soft) so the next + * cold read starts closer. A cache row invalidated by a shrunk log + * (crash-repair truncation) triggers one full re-read from seq 0 — the + * ladder's slow rung, still no crash. Rejects when the session has no + * persisted log (`not found` from the persistence seam). + * @param id - the persisted session to read. + * @param signal - optional cancellation for the persistence reads. + * @returns the snapshot cut at the stored log end. + */ +async coldSnapshot(id: SessionId, signal?: AbortSignal): Promise +``` + +Types: [Session](session.md) · [SessionHeader](persistence.md) · [SessionId](core.md) + +Source: [`packages/session/session-projection-cache/src/index.ts:71`](../../packages/session/session-projection-cache/src/index.ts) + + + +### `ctx.sessionProjections` — `SessionProjectionRegistry` + +`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. Duplicate keys throw. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. + +```ts cordis-catalog +/** + * Register one domain's unit. The registration is an effect on the calling + * context's fiber: disposing the fiber (or calling the returned disposer) + * removes the key — and the unit's cached cells — from subsequent drives + * and snapshots. + * @param definition - key, boundary schema, pure unit functions, and stateVersion. + * @returns the exact disposer that unregisters this unit. + */ +register(definition: ProjectionDefinition): () => void + +/** + * Subscribe to the change feed. The registration is an effect on the + * calling context's fiber. + * @param listener - called once per unit whose state reference changed, per committed event. + * @returns the exact disposer that unsubscribes. + */ +onChanged(listener: ProjectionChangeListener): () => void + +/** + * One consistent cut over every registered unit for one session, read from + * the watermark cache (missing cells fold lazily over the in-memory log). + * Fully synchronous — every value and `asOfSeq` reflect the same log + * position. Each value passes its unit's schema before leaving. + * @param session - the session whose projection values are read. + * @returns the snapshot; `values` is empty when no unit is registered. + */ +snapshot(session: Session): ProjectionSnapshot + +/** + * State-level checkpoint of every registered unit for one session, read + * from the watermark cache (missing cells fold lazily over the in-memory + * log). This is the write side of the persisted projection cache: the + * returned rows are the `(key → {ver, seq, val})` part of the durable + * `(sessionId, key, ver, seq, val)` + * rows. Every `val` is a DETACHED structured clone — never the live + * cell reference: the watermark cache is this registry's authoritative + * mutable state, and a caller reaching the live reference could corrupt + * every subsequent snapshot and frame through it (plain JSON by the unit + * contract, so the clone is total). + * @param session - the session whose unit states are checkpointed. + * @returns one row per registered key; empty when no unit is registered. + */ +checkpoint(session: Session): ProjectionCheckpoint + +/** + * The stored seq a {@link restore} tail read over `checkpoint` must start + * at: one event BELOW the lowest usable watermark (a row is usable when + * its `ver` matches the live unit's `stateVersion`; an absent or mismatched row + * pulls the floor to `0` — that key must refold the full log). The + * one-below anchor is load-bearing: the tail then proves how far the + * stored log still extends, so {@link restore} can detect a log that + * shrank below a row's watermark (crash-repair truncation) instead of + * serving the stale row as current — an empty tail read from the anchor + * yields an end below every watermark and the restore rejects for a full + * re-read. + * @param checkpoint - persisted rows for one session (possibly stale or empty). + * @returns the seq to hand the persistence `readFrom`, or `undefined` + * when no unit is registered (no read needed — {@link restore} would + * serve empty values regardless). + */ +restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined + +/** + * View a checkpoint's rows without any log read: for every registered + * unit whose row's `ver` matches, serve the schema-validated + * `view` of the stored state; mismatched or absent rows leave their key + * absent (a cold or listing consumer treats it as not-yet-available and a + * fuller read path refolds it). The zero-I/O rung of the read ladder — + * values are as stale as their rows, never wrong. + * @param checkpoint - persisted rows for one session (possibly stale or empty). + * @returns whole values per key with a usable row; empty when none. + */ +viewCheckpoint(checkpoint: ProjectionCheckpoint): Partial + +/** + * Cold read: fold every registered unit over a stored log suffix, seeding + * each from its checkpoint row when usable — the one read recipe (cached + * state + forward tail replay + `view`) applied without a live `Session`. + * Call with the events returned by a persistence + * `readFrom(id, restoreFloor(checkpoint))` and that same floor as + * `baseSeq`; the floor's one-below anchor makes the supplied end honest, + * so a shrunk log is detected here. A row is usable iff its + * `ver` matches the live unit's `stateVersion`, it does not predate `baseSeq` + * (`seq >= baseSeq - 1`), and it does not claim events past the + * supplied end (`seq <= endSeq`); an unusable row is discarded + * and its key refolds from `init` — which is only sound over the full + * log, so a discarded row with `baseSeq > 0` throws (the caller re-reads + * from seq 0, e.g. after a crash-repair truncation shrank the log below + * a row's watermark). + * @param checkpoint - persisted rows for one session (possibly stale or empty). + * @param events - the stored events with `seq >= baseSeq`, in seq order. + * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty). + * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last + * supplied event's seq, `baseSeq - 1` for an empty tail) plus the + * refreshed checkpoint rows at that cut, ready for a durable write-back. + */ +restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } +``` + +Types: [Session](session.md) · [SessionEvent](core.md) + +Source: [`packages/session/session-projection/src/index.ts:156`](../../packages/session/session-projection/src/index.ts) + diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md new file mode 100644 index 0000000000..4a2310fed4 --- /dev/null +++ b/docs/subsystems/session-projection.zh.md @@ -0,0 +1,262 @@ +# 会话投影 + +[English](session-projection.md) | 中文 + +会话投影 seam 是一项[能力 seam](../capability-seams.md):领域 host 插件经由它向客户端载体供给按会话的日志派生状态的当前全量值;三方分别是接口与注册表([dsh-session-projection](../../packages/session/session-projection),`ctx.sessionProjections`)、领域贡献方(每个领域注册一个纯单元)与载体(今天是 [dsh-host-apiproxy](../../packages/host/apiproxy) 的历史尾页与 `session/projection` 推送帧)。它是一项可选能力,不属于 agent loop(智能体循环)主干。框架负责驱动,领域负责计算:注册表只订阅一次 `session/event`,并把每个已提交事件折叠进每个单元;领域不持有任何订阅,客户端也从不折叠领域事件——它们收到的是成品值。设计权威:[session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md);驱动、缓存与变更流契约:[包(package)README](../../packages/session/session-projection/README.md)。 + +源码:[`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts) + +## 投影单元 + +`SessionProjectionMap` 是整条链路(host 侧单元、协议块、客户端钩子)的 merge-extensible 类型表;值是协议层 JSON 全量值,渲染归 slot 体系管,永远不归本层。领域为每个 key 贡献一个 `ProjectionDefinition`: + +```ts type-equiv +/** + * One domain's state-driven computation unit: three pure synchronous + * functions plus declarations — never an opaque getter. The framework drives + * `apply` on every committed session event; the domain holds no + * subscriptions and owns only the mathematics. All three functions MUST be + * synchronous (an async unit would tear the carriers' consistency cut) and + * `state` MUST be plain JSON (the persisted-cache precondition). + */ +interface ProjectionDefinition { + /** The projection key this unit owns (its `SessionProjectionMap` entry). */ + key: K + /** Validates the wire payload (`view` output) before it leaves the host. */ + schema: ZodType + /** + * State for the empty log. + * @returns the initial state. + */ + init(): S + /** + * Pure transition: previous state + one committed event → next state. A + * unit uninterested in an event MUST return the same state reference — an + * unchanged reference (`Object.is`) produces zero downstream work. + * @param state - the state covering all prior events. + * @param event - the next committed session event. + * @returns the next state (same reference when the event is not the unit's). + */ + apply(state: S, event: SessionEvent): S + /** + * State → wire payload (the read-side projection). + * @param state - the current state. + * @returns the whole current value for this unit's key. + */ + view(state: S): SessionProjectionMap[K] + /** + * Persisted-cache invalidation anchor: bump whenever the state shape or the + * fold semantics change, so persisted `(sessionId, key, ver, seq, val)` + * rows from an older unit are discarded instead of being forward-applied + * into garbage. Non-negative integer. + */ + stateVersion: number +} +``` + +全量值事件规则是承重结构:携带状态的日志事件携带的是变更后的完整状态,绝不是裸增量——这让每次状态转移始终足够廉价,也让每个被供给的值自描述(对消费方即 last-wins)。 + +## 快照与变更流 + +```ts type-equiv +/** + * One consistent read cut over every registered unit for one session. + * `asOfSeq` is the shared watermark — the seq of the last event every value + * reflects (`-1` for an empty log, mirroring `session/subscribed.lastSeq`). + */ +interface ProjectionSnapshot { + /** Seq of the last event the values reflect; -1 for an empty log. */ + asOfSeq: number + /** Whole current value per registered key. */ + values: Partial +} +``` + +```ts type-equiv +/** + * Change-feed listener: one unit's value changed for one session. `value` is + * the schema-validated `view` output; `seq` is the unit's watermark at + * emission (the seq of the event that caused the change). + */ +type ProjectionChangeListener = ( + session: Session, + key: Extract, + value: unknown, + seq: number, +) => void +``` + +`snapshot(session)` 是完全同步的:载体在切出页面切片的同一 tick 内读取它,`asOfSeq` 之所以是一个一致切面正系于此;且每个值在离开前都要经过其单元的 schema 校验(误写成异步的 `view` 会返回 Promise,让这道边界解析当场大声失败)。变更流对每个已提交事件、每个状态*引用*发生变化的单元各触发一次:`apply` 的同引用纪律就是那道闸门。 + +## 注册表:`ctx.sessionProjections` + +`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动权:一份 `session/event` 订阅、对每个已注册单元的正向 `apply`,以及每会话每单元的水位线(watermark)cell。cell 惰性构建:在事件流过之后才注册的单元,或比注册表更早的会话,都在首次触达(事件或读取)时从 `init` 出发在内存日志上折叠。注册是一个 effect,其 disposer 随调用方 fiber 走:领域插件卸载后,其 key(连同缓存的 cell)从后续驱动与快照中消失,客户端将其读作能力缺失;key 重复直接 throw。领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionProjectionCache` — `SessionProjectionCache` + +The persisted projection cache service. Opens the `session_projcache` domain at init, checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus two mandatory points — `turn/end` and session disposal (the live-to-cold moment) — and serves the cold-read ladder: cached row, persistence `readFrom` tail, registry `restore`, durable write-back. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write or cold read. + +```ts cordis-catalog +/** + * The zero-I/O listing read: whole values viewed straight from the stored + * rows (version-matching keys only), each cut carried with its watermark + * so a client value store can seed under its higher-seq-wins rule — as + * stale as the last durable checkpoint but never wrong, and never from an + * unrelated log (the caller's header is the identity witness). Fresher + * paths (the history tail baseline, {@link coldSnapshot}) supersede these + * values whenever a session is actually opened. + * @param meta - the listed session's header (identity witness; no log read). + * @returns the cut (`asOfSeq` = lowest served-row watermark), or + * `undefined` when no usable row exists for this lifecycle. + */ +cachedSnapshot(meta: SessionHeader): ProjectionSnapshot | undefined + +/** + * Durably checkpoint one live session NOW (both mandatory points call + * this; tests and carriers may too). The registry cut is snapshotted at + * this boundary (states are live references), then the whole record is + * replaced. NOT fail-soft — callers on the fail-soft paths contain it. + * @param session - the live session to checkpoint. + * @returns resolution after durability and event emission. + */ +async write(session: Session): Promise + +/** + * Cold-read one persisted session's projections with zero full-log load: + * cached rows + a persistence `readFrom` tail from the registry's restore + * floor, refolded by the registry and written back (fail-soft) so the next + * cold read starts closer. A cache row invalidated by a shrunk log + * (crash-repair truncation) triggers one full re-read from seq 0 — the + * ladder's slow rung, still no crash. Rejects when the session has no + * persisted log (`not found` from the persistence seam). + * @param id - the persisted session to read. + * @param signal - optional cancellation for the persistence reads. + * @returns the snapshot cut at the stored log end. + */ +async coldSnapshot(id: SessionId, signal?: AbortSignal): Promise +``` + +Types: [Session](session.md) · [SessionHeader](persistence.md) · [SessionId](core.md) + +Source: [`packages/session/session-projection-cache/src/index.ts:71`](../../packages/session/session-projection-cache/src/index.ts) + + + +### `ctx.sessionProjections` — `SessionProjectionRegistry` + +`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. Duplicate keys throw. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. + +```ts cordis-catalog +/** + * Register one domain's unit. The registration is an effect on the calling + * context's fiber: disposing the fiber (or calling the returned disposer) + * removes the key — and the unit's cached cells — from subsequent drives + * and snapshots. + * @param definition - key, boundary schema, pure unit functions, and stateVersion. + * @returns the exact disposer that unregisters this unit. + */ +register(definition: ProjectionDefinition): () => void + +/** + * Subscribe to the change feed. The registration is an effect on the + * calling context's fiber. + * @param listener - called once per unit whose state reference changed, per committed event. + * @returns the exact disposer that unsubscribes. + */ +onChanged(listener: ProjectionChangeListener): () => void + +/** + * One consistent cut over every registered unit for one session, read from + * the watermark cache (missing cells fold lazily over the in-memory log). + * Fully synchronous — every value and `asOfSeq` reflect the same log + * position. Each value passes its unit's schema before leaving. + * @param session - the session whose projection values are read. + * @returns the snapshot; `values` is empty when no unit is registered. + */ +snapshot(session: Session): ProjectionSnapshot + +/** + * State-level checkpoint of every registered unit for one session, read + * from the watermark cache (missing cells fold lazily over the in-memory + * log). This is the write side of the persisted projection cache: the + * returned rows are the `(key → {ver, seq, val})` part of the durable + * `(sessionId, key, ver, seq, val)` + * rows. Every `val` is a DETACHED structured clone — never the live + * cell reference: the watermark cache is this registry's authoritative + * mutable state, and a caller reaching the live reference could corrupt + * every subsequent snapshot and frame through it (plain JSON by the unit + * contract, so the clone is total). + * @param session - the session whose unit states are checkpointed. + * @returns one row per registered key; empty when no unit is registered. + */ +checkpoint(session: Session): ProjectionCheckpoint + +/** + * The stored seq a {@link restore} tail read over `checkpoint` must start + * at: one event BELOW the lowest usable watermark (a row is usable when + * its `ver` matches the live unit's `stateVersion`; an absent or mismatched row + * pulls the floor to `0` — that key must refold the full log). The + * one-below anchor is load-bearing: the tail then proves how far the + * stored log still extends, so {@link restore} can detect a log that + * shrank below a row's watermark (crash-repair truncation) instead of + * serving the stale row as current — an empty tail read from the anchor + * yields an end below every watermark and the restore rejects for a full + * re-read. + * @param checkpoint - persisted rows for one session (possibly stale or empty). + * @returns the seq to hand the persistence `readFrom`, or `undefined` + * when no unit is registered (no read needed — {@link restore} would + * serve empty values regardless). + */ +restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined + +/** + * View a checkpoint's rows without any log read: for every registered + * unit whose row's `ver` matches, serve the schema-validated + * `view` of the stored state; mismatched or absent rows leave their key + * absent (a cold or listing consumer treats it as not-yet-available and a + * fuller read path refolds it). The zero-I/O rung of the read ladder — + * values are as stale as their rows, never wrong. + * @param checkpoint - persisted rows for one session (possibly stale or empty). + * @returns whole values per key with a usable row; empty when none. + */ +viewCheckpoint(checkpoint: ProjectionCheckpoint): Partial + +/** + * Cold read: fold every registered unit over a stored log suffix, seeding + * each from its checkpoint row when usable — the one read recipe (cached + * state + forward tail replay + `view`) applied without a live `Session`. + * Call with the events returned by a persistence + * `readFrom(id, restoreFloor(checkpoint))` and that same floor as + * `baseSeq`; the floor's one-below anchor makes the supplied end honest, + * so a shrunk log is detected here. A row is usable iff its + * `ver` matches the live unit's `stateVersion`, it does not predate `baseSeq` + * (`seq >= baseSeq - 1`), and it does not claim events past the + * supplied end (`seq <= endSeq`); an unusable row is discarded + * and its key refolds from `init` — which is only sound over the full + * log, so a discarded row with `baseSeq > 0` throws (the caller re-reads + * from seq 0, e.g. after a crash-repair truncation shrank the log below + * a row's watermark). + * @param checkpoint - persisted rows for one session (possibly stale or empty). + * @param events - the stored events with `seq >= baseSeq`, in seq order. + * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty). + * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last + * supplied event's seq, `baseSeq - 1` for an empty tail) plus the + * refreshed checkpoint rows at that cut, ready for a durable write-back. + */ +restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } +``` + +Types: [Session](session.md) · [SessionEvent](core.md) + +Source: [`packages/session/session-projection/src/index.ts:156`](../../packages/session/session-projection/src/index.ts) + diff --git a/docs/subsystems/session-query.i18n.yaml b/docs/subsystems/session-query.i18n.yaml index eca4715ba9..728a6cf8db 100644 --- a/docs/subsystems/session-query.i18n.yaml +++ b/docs/subsystems/session-query.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/session-query.md -session-query.md: e7514dd6c3bc20a07395663bff40ce65e1363b78 -session-query.zh.md: 4c3dd4d435dbd8a20fbd4db5da1a7d649c2e6d0b +# pnpm run verify-translation-pairing --write docs/subsystems/session-query.md +session-query.md: 54ae1eaf34a17d4979a621e4fc310780b425364e +session-query.zh.md: b770514757c30aa5d87b25e5d280b1ce6710cd9c diff --git a/docs/subsystems/session-query.md b/docs/subsystems/session-query.md index e7514dd6c3..54ae1eaf34 100644 --- a/docs/subsystems/session-query.md +++ b/docs/subsystems/session-query.md @@ -354,3 +354,141 @@ type SessionQueryErrorCode = | 'SESSION_QUERY_STALE_CURSOR' | 'SESSION_QUERY_SOURCE_CONFLICT' ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionQuery` — `SessionQueryService` (abstract seam) + +Unified live-preferred session query service. + +Exact reads, filters, and traces are backend-independent concrete behavior. A backend implements full-text observation, reconciliation, ranking, cursor generations, and query execution on the same `ctx.sessionQuery` service. + +```ts cordis-catalog +/** + * Search the live-preferred logical corpus and group by session. + * @param request - query text, metadata filters, page size, and cursor. + * @param exec - optional cancellation control. + * @returns session hits ranked by their strongest matching event. + */ +abstract searchSessions( request: SessionSearchRequest, exec?: SessionSearchExecContext, ): Promise> + +/** + * Search events within one live-preferred logical session. + * @param request - target session, query text, filters, page size, and cursor. + * @param exec - optional cancellation control. + * @returns matching event hits and their target header from one indexed generation. + */ +abstract searchEvents( request: SessionEventSearchRequest, exec?: SessionSearchExecContext, ): Promise + +/** + * List the complete logical corpus using live-preferred records. + * @param signal - optional cancellation for persistence listing. + * @returns deterministic newest-first cloned session records. + */ +listSessions(signal?: AbortSignal): Promise + +/** + * Read and replay-validate one complete logical session log without making it live. + * @param sessionId - live or persisted session id to read. + * @returns cloned header and complete raw event log from one observation. + * @throws when persistence, header compatibility, or replay validation fails. + */ +async readSession(sessionId: SessionId): Promise + +/** + * Filter the complete logical corpus with provider-independent predicates. + * @param filters - ANDed session metadata and availability clauses. + * @param signal - optional cancellation for persistence listing. + * @returns matching cloned records in deterministic newest-first order. + */ +async filterSessions( filters: readonly SessionResultFilter[], signal?: AbortSignal, ): Promise + +/** + * Fold the latest log-backed title from one live-preferred logical session. + * @param sessionId - live or persisted session id to read. + * @param signal - optional cancellation for source resolution and title folding. + * @returns latest title snapshot, or `undefined` when the log has no title event. + */ +async readTitle( sessionId: SessionId, signal?: AbortSignal, ): Promise + +/** + * Fold the latest title and return its source header from one corpus observation. + * @param sessionId - live or persisted session id to read. + * @param signal - optional cancellation for source resolution and title folding. + * @returns cloned source header and optional latest title snapshot. + */ +async readTitleSnapshot( sessionId: SessionId, signal?: AbortSignal, ): Promise + +/** + * Fold titles for unique sessions from one cancellable corpus observation. + * + * Results preserve first-occurrence input order. Operational failures stay + * isolated per session, while cancellation rejects the complete operation. + * @param sessionIds - live or persisted session ids to observe. + * @param signal - optional cancellation shared by all source reads. + * @returns one fulfilled or rejected result per unique requested id. + */ +async readTitleSnapshots( sessionIds: readonly SessionId[], signal?: AbortSignal, ): Promise + +/** + * List lightweight raw-log event records for one logical session. + * @param sessionId - live-preferred session id to read. + * @returns event records in ascending seq order. + */ +async listEvents(sessionId: SessionId): Promise + +/** + * Scan first-party semantic event documents with provider-independent filters. + * @param sessionId - live-preferred session id to scan. + * @param filters - ANDed metadata and literal-text predicates. + * @returns matching semantic documents in ascending seq order. + */ +async filterEvents( sessionId: SessionId, filters: readonly SessionEventResultFilter[], ): Promise + +/** + * Read one session's complete current model surface from one corpus observation. + * @param sessionId - live-preferred session id to read. + * @returns cloned header, current surface, and raw-log capture boundary. + * @throws when source resolution fails or the session surface is invalid. + */ +async readSurface(sessionId: SessionId): Promise + +/** + * Trace known ancestry and descendants from one corpus observation. + * @param sessionId - logical session id to trace. + * @param signal - optional cancellation for persistence listing. + * @returns a complete lineage or an explicit unresolved parent boundary. + * @throws when corpus resolution fails, the target is absent, or its known ancestry cycles. + */ +async traceSession(sessionId: SessionId, signal?: AbortSignal): Promise + +/** + * Trace one event's direct positional and provenance relationships. + * @param request - target session id and event seq. + * @param signal - optional cancellation for persisted source resolution. + * @returns source header, direct links, and the target's positional replacement chain. + * @throws when source resolution fails, the target is absent, or surface/provenance validation fails. + */ +async traceEvent(request: SessionEventTraceRequest, signal?: AbortSignal): Promise + +/** + * Read one full event plus a bounded raw-log context window. + * @param request - target session/seq and context sizes. + * @param signal - optional cancellation for persisted source resolution. + * @returns cloned target and neighboring events. + */ +async readEvent(request: SessionEventReadRequest, signal?: AbortSignal): Promise +``` + +Types: [SessionId](core.md) · [SessionTitleSnapshot](session-title.md) + +Source: [`packages/session-query/session-query/src/index.ts:81`](../../packages/session-query/session-query/src/index.ts) + diff --git a/docs/subsystems/session-query.zh.md b/docs/subsystems/session-query.zh.md index 4c3dd4d435..b770514757 100644 --- a/docs/subsystems/session-query.zh.md +++ b/docs/subsystems/session-query.zh.md @@ -354,3 +354,141 @@ type SessionQueryErrorCode = | 'SESSION_QUERY_STALE_CURSOR' | 'SESSION_QUERY_SOURCE_CONFLICT' ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionQuery` — `SessionQueryService` (abstract seam) + +Unified live-preferred session query service. + +Exact reads, filters, and traces are backend-independent concrete behavior. A backend implements full-text observation, reconciliation, ranking, cursor generations, and query execution on the same `ctx.sessionQuery` service. + +```ts cordis-catalog +/** + * Search the live-preferred logical corpus and group by session. + * @param request - query text, metadata filters, page size, and cursor. + * @param exec - optional cancellation control. + * @returns session hits ranked by their strongest matching event. + */ +abstract searchSessions( request: SessionSearchRequest, exec?: SessionSearchExecContext, ): Promise> + +/** + * Search events within one live-preferred logical session. + * @param request - target session, query text, filters, page size, and cursor. + * @param exec - optional cancellation control. + * @returns matching event hits and their target header from one indexed generation. + */ +abstract searchEvents( request: SessionEventSearchRequest, exec?: SessionSearchExecContext, ): Promise + +/** + * List the complete logical corpus using live-preferred records. + * @param signal - optional cancellation for persistence listing. + * @returns deterministic newest-first cloned session records. + */ +listSessions(signal?: AbortSignal): Promise + +/** + * Read and replay-validate one complete logical session log without making it live. + * @param sessionId - live or persisted session id to read. + * @returns cloned header and complete raw event log from one observation. + * @throws when persistence, header compatibility, or replay validation fails. + */ +async readSession(sessionId: SessionId): Promise + +/** + * Filter the complete logical corpus with provider-independent predicates. + * @param filters - ANDed session metadata and availability clauses. + * @param signal - optional cancellation for persistence listing. + * @returns matching cloned records in deterministic newest-first order. + */ +async filterSessions( filters: readonly SessionResultFilter[], signal?: AbortSignal, ): Promise + +/** + * Fold the latest log-backed title from one live-preferred logical session. + * @param sessionId - live or persisted session id to read. + * @param signal - optional cancellation for source resolution and title folding. + * @returns latest title snapshot, or `undefined` when the log has no title event. + */ +async readTitle( sessionId: SessionId, signal?: AbortSignal, ): Promise + +/** + * Fold the latest title and return its source header from one corpus observation. + * @param sessionId - live or persisted session id to read. + * @param signal - optional cancellation for source resolution and title folding. + * @returns cloned source header and optional latest title snapshot. + */ +async readTitleSnapshot( sessionId: SessionId, signal?: AbortSignal, ): Promise + +/** + * Fold titles for unique sessions from one cancellable corpus observation. + * + * Results preserve first-occurrence input order. Operational failures stay + * isolated per session, while cancellation rejects the complete operation. + * @param sessionIds - live or persisted session ids to observe. + * @param signal - optional cancellation shared by all source reads. + * @returns one fulfilled or rejected result per unique requested id. + */ +async readTitleSnapshots( sessionIds: readonly SessionId[], signal?: AbortSignal, ): Promise + +/** + * List lightweight raw-log event records for one logical session. + * @param sessionId - live-preferred session id to read. + * @returns event records in ascending seq order. + */ +async listEvents(sessionId: SessionId): Promise + +/** + * Scan first-party semantic event documents with provider-independent filters. + * @param sessionId - live-preferred session id to scan. + * @param filters - ANDed metadata and literal-text predicates. + * @returns matching semantic documents in ascending seq order. + */ +async filterEvents( sessionId: SessionId, filters: readonly SessionEventResultFilter[], ): Promise + +/** + * Read one session's complete current model surface from one corpus observation. + * @param sessionId - live-preferred session id to read. + * @returns cloned header, current surface, and raw-log capture boundary. + * @throws when source resolution fails or the session surface is invalid. + */ +async readSurface(sessionId: SessionId): Promise + +/** + * Trace known ancestry and descendants from one corpus observation. + * @param sessionId - logical session id to trace. + * @param signal - optional cancellation for persistence listing. + * @returns a complete lineage or an explicit unresolved parent boundary. + * @throws when corpus resolution fails, the target is absent, or its known ancestry cycles. + */ +async traceSession(sessionId: SessionId, signal?: AbortSignal): Promise + +/** + * Trace one event's direct positional and provenance relationships. + * @param request - target session id and event seq. + * @param signal - optional cancellation for persisted source resolution. + * @returns source header, direct links, and the target's positional replacement chain. + * @throws when source resolution fails, the target is absent, or surface/provenance validation fails. + */ +async traceEvent(request: SessionEventTraceRequest, signal?: AbortSignal): Promise + +/** + * Read one full event plus a bounded raw-log context window. + * @param request - target session/seq and context sizes. + * @param signal - optional cancellation for persisted source resolution. + * @returns cloned target and neighboring events. + */ +async readEvent(request: SessionEventReadRequest, signal?: AbortSignal): Promise +``` + +Types: [SessionId](core.md) · [SessionTitleSnapshot](session-title.md) + +Source: [`packages/session-query/session-query/src/index.ts:81`](../../packages/session-query/session-query/src/index.ts) + diff --git a/docs/subsystems/session-reference.i18n.yaml b/docs/subsystems/session-reference.i18n.yaml index f119c67913..980c1a1a2d 100644 --- a/docs/subsystems/session-reference.i18n.yaml +++ b/docs/subsystems/session-reference.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-reference.md -session-reference.md: 5375677f6a1748909743ca76d5191cb9e736a40a -session-reference.zh.md: 3ff4a1719926bda0a9111482a7778a8c94553370 +session-reference.md: 60e29c56ea372e100009b94caedea5327308432a +session-reference.zh.md: 3ae9264abc15848646e5397dfcbeff764c038918 diff --git a/docs/subsystems/session-reference.md b/docs/subsystems/session-reference.md index 5375677f6a..60e29c56ea 100644 --- a/docs/subsystems/session-reference.md +++ b/docs/subsystems/session-reference.md @@ -65,3 +65,44 @@ type SessionReferenceErrorCode = | 'SESSION_REFERENCE_BUDGET_EXCEEDED' | 'SESSION_REFERENCE_CANCELLED' ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionReferences` — `SessionReferenceService` + +Exact-read consumer that prepares immutable cross-session message context. + +```ts cordis-catalog +/** + * List reference candidates, ranked by working-directory affinity. + * @param agent - target agent; self is excluded and its cwd drives ranking. + * @param query - optional case-insensitive session-id/cwd/title substring. + * @param limit - optional positive result cap. + * @param signal - optional cancellation boundary for host autocomplete teardown. + * @returns candidates labeled by latest title or, when absent, session id. + */ +async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise + +/** + * Snapshot all references before enqueue and return one aggregated durable context. + * @param agent - target agent; references to it are rejected. + * @param content - already host-normalized readable message content. + * @param references - structured source sessions in mention order. + * @param signal - optional cancellation boundary for host request teardown. + * @returns detached content and optional referenced-session context. + */ +async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise +``` + +Types: [Agent](core.md) · [ContentBlock](core.md) + +Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts) + diff --git a/docs/subsystems/session-reference.zh.md b/docs/subsystems/session-reference.zh.md index 3ff4a17199..3ae9264abc 100644 --- a/docs/subsystems/session-reference.zh.md +++ b/docs/subsystems/session-reference.zh.md @@ -65,3 +65,44 @@ type SessionReferenceErrorCode = | 'SESSION_REFERENCE_BUDGET_EXCEEDED' | 'SESSION_REFERENCE_CANCELLED' ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionReferences` — `SessionReferenceService` + +Exact-read consumer that prepares immutable cross-session message context. + +```ts cordis-catalog +/** + * List reference candidates, ranked by working-directory affinity. + * @param agent - target agent; self is excluded and its cwd drives ranking. + * @param query - optional case-insensitive session-id/cwd/title substring. + * @param limit - optional positive result cap. + * @param signal - optional cancellation boundary for host autocomplete teardown. + * @returns candidates labeled by latest title or, when absent, session id. + */ +async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise + +/** + * Snapshot all references before enqueue and return one aggregated durable context. + * @param agent - target agent; references to it are rejected. + * @param content - already host-normalized readable message content. + * @param references - structured source sessions in mention order. + * @param signal - optional cancellation boundary for host request teardown. + * @returns detached content and optional referenced-session context. + */ +async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise +``` + +Types: [Agent](core.md) · [ContentBlock](core.md) + +Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts) + diff --git a/docs/subsystems/session-title.i18n.yaml b/docs/subsystems/session-title.i18n.yaml index c85d4dfac1..8b58415234 100644 --- a/docs/subsystems/session-title.i18n.yaml +++ b/docs/subsystems/session-title.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-title.md -session-title.md: 7bced67f07df3f00471f02b766922cdc3e4fd29e -session-title.zh.md: e8e501d3aafee49395da3d9a8efd4e91a724f09e +session-title.md: 3a2df284f2af14def284e502f4e37daaf4d7a262 +session-title.zh.md: 3a74baa9005459b35b658df95bdd7b57256b3e2e diff --git a/docs/subsystems/session-title.md b/docs/subsystems/session-title.md index 7bced67f07..3a2df284f2 100644 --- a/docs/subsystems/session-title.md +++ b/docs/subsystems/session-title.md @@ -144,3 +144,61 @@ interface SessionTitleProvider { generate(request: SessionTitleProviderRequest): Promise } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionTitle` — `SessionTitleService` + +Log-backed title fold plus asynchronous fallback generation. + +```ts cordis-catalog +/** + * Read the latest folded title from one live or replayed session. + * @param session - session whose log is the title source of truth. + * @returns latest title snapshot, or `undefined` before eligible input. + */ +get(session: Session): SessionTitleSnapshot | undefined + +/** + * Accept an explicit user title. Appends a `session/title` event with the + * `user` source, which pins the title: in-flight automatic generation is + * superseded and later user messages schedule none (an explicit + * {@link SessionTitleService.refresh} remains the deliberate unpin). + * @param session - exact live session to rename. + * @param title - raw user input; normalized before acceptance. + * @returns the accepted title snapshot. + * @throws {SessionTitleInvalidError} when the title normalizes to empty. + * @throws {Error} when the session is not live or the service is disposed. + */ +rename(session: Session, title: string): SessionTitleSnapshot + +/** + * Explicitly retry the registered provider, or materialize the built-in + * fallback when no provider is registered. + * @param session - exact live session to refresh. + * @param signal - optional caller cancellation. + * @returns latest accepted title, or `undefined` when no eligible text exists. + */ +async refresh(session: Session, signal?: AbortSignal): Promise + +/** + * Register the sole optional title provider. Disposal aborts its pending and + * active work before another provider may register. + * @param provider - provider identity, cadence, and generation function. + * @returns exact Cordis effect disposer, which settles after active calls quiesce. + */ +register(provider: SessionTitleProvider): () => Promise +``` + +Types: [Session](session.md) + +Source: [`packages/session/session-title/src/index.ts:261`](../../packages/session/session-title/src/index.ts) + diff --git a/docs/subsystems/session-title.zh.md b/docs/subsystems/session-title.zh.md index e8e501d3aa..3a74baa900 100644 --- a/docs/subsystems/session-title.zh.md +++ b/docs/subsystems/session-title.zh.md @@ -144,3 +144,61 @@ interface SessionTitleProvider { generate(request: SessionTitleProviderRequest): Promise } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessionTitle` — `SessionTitleService` + +Log-backed title fold plus asynchronous fallback generation. + +```ts cordis-catalog +/** + * Read the latest folded title from one live or replayed session. + * @param session - session whose log is the title source of truth. + * @returns latest title snapshot, or `undefined` before eligible input. + */ +get(session: Session): SessionTitleSnapshot | undefined + +/** + * Accept an explicit user title. Appends a `session/title` event with the + * `user` source, which pins the title: in-flight automatic generation is + * superseded and later user messages schedule none (an explicit + * {@link SessionTitleService.refresh} remains the deliberate unpin). + * @param session - exact live session to rename. + * @param title - raw user input; normalized before acceptance. + * @returns the accepted title snapshot. + * @throws {SessionTitleInvalidError} when the title normalizes to empty. + * @throws {Error} when the session is not live or the service is disposed. + */ +rename(session: Session, title: string): SessionTitleSnapshot + +/** + * Explicitly retry the registered provider, or materialize the built-in + * fallback when no provider is registered. + * @param session - exact live session to refresh. + * @param signal - optional caller cancellation. + * @returns latest accepted title, or `undefined` when no eligible text exists. + */ +async refresh(session: Session, signal?: AbortSignal): Promise + +/** + * Register the sole optional title provider. Disposal aborts its pending and + * active work before another provider may register. + * @param provider - provider identity, cadence, and generation function. + * @returns exact Cordis effect disposer, which settles after active calls quiesce. + */ +register(provider: SessionTitleProvider): () => Promise +``` + +Types: [Session](session.md) + +Source: [`packages/session/session-title/src/index.ts:261`](../../packages/session/session-title/src/index.ts) + diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index dd7135e8d3..a7e81056e5 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/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: 5d54b03df8ec3345e8bf04702f242e3aacf8ec39 -session.zh.md: 5a867cfe302bf02994bfe5a5a704bed55222e8ea +session.md: 28b3f959b47a24373860c2334f9273286d072c95 +session.zh.md: 5870265f55892231700b65ac6ed69d827a0ab1ec diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 5d54b03df8..28b3f959b4 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -343,7 +343,7 @@ interface SurfaceFoldResult { ## `Session` public API -The body-stripped declaration keeps the plain class's detached factory, state accessors, append boundary, and history projections synchronized with source. Store operations remain in the generated [`ctx.sessions` service catalog](../cordis-catalog/services.md#ctxsessions--sessionstore). +The body-stripped declaration keeps the plain class's detached factory, state accessors, append boundary, and history projections synchronized with source. Store operations remain in the generated [`ctx.sessions` section](#ctxsessions--sessionstore). ```ts public-api /** @@ -586,3 +586,247 @@ The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepse What a persistence backend relies on: the durable log persists every event losslessly, **including** `assistant/chunk` — `seq` must stay contiguous, so chunks cannot be filtered out of the canonical log. A backend may choose its own storage encoding for an event batch as long as `load` returns the exact appended events (the JSONL backend's default packed chunk rows are such an encoding — see [persistence.md](persistence.md)). All `event.data` must be JSON-serializable; `Session.append` enforces this at the source (throwing on non-serializable data), so a bad event never enters the log and `session.events` always equals what a backend can persist. Adding an event type that carries non-serializable data, corrupts core execution nesting, or violates its owner's declared relation is a breaking change to the on-disk format. The backends that consume this contract are on [persistence.md](persistence.md). + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessions` — `SessionStore` + +In-memory session store (`ctx.sessions`). + +Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose. + +```ts cordis-catalog +/** + * Create a session owned by the calling fiber: disposing that fiber stops + * event notification and removes the session from the store. `options.seed` + * populates the session with a copy of those events (replay/fork); + * `options.meta` attaches creation metadata (validated absolute `cwd`, seed + * and parent lineage, and delegation depth) as the immutable + * {@link SessionHeader} (the store fills `version`/`id`/`createdAt`). + * + * For an agent whose session must be torn down IN ORDER with its loop (so the + * loop's final events are published before the store attachment ends), do NOT use this + * — fold the session lifecycle into the agent's own effect via + * {@link prepare} + {@link enter} + {@link announce} (see + * `dsh-agent-loop`'s creation transaction). + * + * @param id - the session id; omitted, the store mints `session-`. + * @param options - seed events and/or creation metadata for the header. + * @returns the live session, already entered and announced. + * @throws if a session with `id` already exists, metadata is not a plain + * lossless-JSON record with valid scalar fields, or `meta.cwd` is a + * non-absolute path (storage backends key directories off it). + */ +create(id?: SessionId, options?: CreateSessionOptions): Session + +/** + * Build a session WITHOUT entering it into the store — validate the id/cwd and + * construct the {@link Session} (with its immutable {@link SessionHeader}). + * Pairs with {@link enter} + {@link announce}: a caller that owns a composite + * `ctx.effect` (the agent factory) folds the session lifecycle into that ONE + * effect so a fiber unload tears the session + agent down as a single ORDERED + * chain rather than as racing sibling effects — which would remove the publication hooks + * before the driver's closing events commit, dropping them. + * + * @param id - the session id; omitted, the store mints `session-`. + * @param options - seed events and/or creation metadata for the header. With + * `seedSource: 'persistence'`, metadata and events must be fresh detached + * graphs whose ownership transfers to this call: they are validated and + * frozen in place through {@link Session.fromRestore}, so the caller must + * retain no mutable aliases. + * @returns the constructed session, NOT yet in the store. + * @throws if a session with `id` already exists, metadata is not a plain + * lossless-JSON record with valid scalar fields, or `meta.cwd` is a + * non-absolute path. + */ +prepare(id?: SessionId, options?: PrepareSessionOptions): Session + +/** + * Enter a {@link prepare}d session into the store: install the module-private + * append publication hooks and add it to the store. Returns the DETACH + * disposer (hooks + store removal). Does NOT emit `session/created` — + * the caller yields this disposer inside its effect and THEN calls + * {@link announce}, so a throwing `session/created` listener rolls the attach + * back instead of leaking it. + * + * Re-checks the id for a duplicate: `prepare` and `enter` are public + * cross-package primitives and a caller may interleave arbitrary work (or + * another create) between them, so a stale prepared session must NOT overwrite + * a live store entry of the same id — its detach disposer would later delete + * the REAL session. The {@link create} convenience and the agent factory call + * the two back-to-back so they never trip this, but the public seam cannot + * assume that. + * + * @param session - a {@link prepare}d session not yet in the store. + * @returns the detach disposer (publication hooks + store removal). When called from + * a synchronous `session/created` listener, removal and disposal wait until + * that creation dispatch unwinds. + * @throws if a session with this id is already in the store. + */ +enter(session: Session): () => void + +/** Emit `session/created` exactly once for an {@link enter}ed session (with + * the carrier {@link enter} captured). Separate from {@link enter} so the + * caller can yield the detach disposer first (rollback safety — see + * {@link enter}). + * @param session - the entered session to announce to listeners. + * @throws if the session is not live or its announcement already began, + * including a reentrant call from a creation listener. */ +announce(session: Session): void + +/** + * Dispatch the awaited `session/flush` durability checkpoint for `session`, + * with the carrier captured at {@link enter}. THE flush entry point: the + * store owns the carrier, so callers (the checkpoint policy's per-request + * barrier, goal-session's idle checkpoint, teardown drains, and consumers + * that flush themselves before reading storage) must come through here + * rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner, + * one spelling, and the scoped-dispatch invariant can pin it. + * @param session - the session whose buffered events must reach durable storage. + * @returns whether at least one durability listener participated, after every + * listener has settled successfully. + * @throws the first registered listener failure after every listener settles. + */ +async flush(session: Session): Promise + +/** + * Look up a live session. + * @param id - the session id to look up. + * @returns the session, or undefined when no live session has that id. + */ +get(id: SessionId): Session | undefined + +/** + * All live sessions, in creation order. + * @returns a fresh array; mutating it does not affect the store. + */ +list(): Session[] + +/** + * Create a live child session from a stable prefix of a live source. + * `boundary` is an inclusive source event seq; omitted means the source's + * current last event. The selected slice may end with a between-turn event + * but must not end inside an open turn. + * + * @param source - Live source session object or id. + * @param boundary - Inclusive source event seq to fork through; omitted means + * the source's current last event, and omitted on an empty source forks an + * empty child. + * @param childSessionId - Optional child session id; omitted delegates to + * `SessionStore`'s id policy. + * @returns The created live child session. + */ +fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session +``` + +Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md) + +Source: [`packages/core/session/src/index.ts:807`](../../packages/core/session/src/index.ts) + + + +### `session/*` events + + + +#### `session/created` — emit + +Creation announcement during session publication. A synchronous throw vetoes and rolls back with a paired disposal; detach requested during dispatch is deferred. A returned-promise rejection is logged but cannot retroactively veto this synchronous boundary. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only sessions entered through that agent's context. + +```ts cordis-catalog +/** + * Creation announcement during session publication. A synchronous throw vetoes and rolls + * back with a paired disposal; detach requested during dispatch is deferred. + * A returned-promise rejection is logged but cannot retroactively veto this + * synchronous boundary. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners + * receive only sessions entered through that agent's context. + * @param session - the session just entered and announced. + * @dshScopeScan unsupported + * @mode emit + */ +'session/created'(this: Scoped, session: Session): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/session/src/index.ts:74`](../../packages/core/session/src/index.ts) + + + +#### `session/disposed` — emit + +Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin. Listener failures are logged and contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. + +```ts cordis-catalog +/** + * Emitted once when an announced session leaves the store, including + * publication rollback, but never for an entry whose creation announcement + * did not begin. Listener failures are logged and contained. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. + * @param session - the session that is no longer live in the store. + * @dshScopeScan unsupported + * @mode emit + */ +'session/disposed'(this: Scoped, session: Session): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/session/src/index.ts:84`](../../packages/core/session/src/index.ts) + + + +#### `session/event` — emit + +Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context. + +```ts cordis-catalog +/** + * Post-commit, fire-and-forget append feed. The listener snapshot resolves + * before the log push, but callbacks run after it; observer failures are + * logged and contained without making the committed append fail. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners + * receive only events from sessions entered through that agent's context. + * @param session - the session whose log grew. + * @param event - the appended event, exactly as recorded. + * @dshScopeScan unsupported + * @mode emit + */ +'session/event'(this: Scoped, session: Session, event: SessionEvent): void +``` + +Types: [Scoped](scope.md) · [SessionEvent](core.md) + +Source: [`packages/core/session/src/index.ts:96`](../../packages/core/session/src/index.ts) + + + +#### `session/flush` — parallel + +Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. + +```ts cordis-catalog +/** + * Awaited parallel durability checkpoint: every listener runs and the + * caller awaits all of them, with no waterfall veto. Scope-filtered dispatch + * (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. + * @param session - the session whose buffered events must reach durable storage. + * @dshScopeScan unsupported + * @mode parallel + */ +'session/flush'(this: Scoped, session: Session): Promise | void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/session/src/index.ts:105`](../../packages/core/session/src/index.ts) + diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index 5a867cfe30..5870265f55 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -345,7 +345,7 @@ interface SurfaceFoldResult { ## `Session` 公共 API -去除方法体的声明与源码中的普通类保持同步,覆盖其脱离态工厂、状态访问器、追加边界和历史投影。存储操作仍由生成的 [`ctx.sessions` 服务目录](../cordis-catalog/services.md#ctxsessions--sessionstore)记录。 +去除方法体的声明与源码中的普通类保持同步,覆盖其脱离态工厂、状态访问器、追加边界和历史投影。存储操作仍由生成的 [`ctx.sessions` 小节](#ctxsessions--sessionstore)记录。 ```ts public-api /** @@ -590,3 +590,247 @@ interface TurnEndReasonMap { 持久化后端依赖的契约如下:持久日志无损保存每个事件,**包括** `assistant/chunk`;`seq` 必须连续,因此不能从规范日志中过滤分片。后端可以为事件批次选择自己的存储编码,只要 `load` 返回与追加时完全一致的事件即可(JSONL 后端默认启用的打包分片行就是此类编码;见 [persistence.md](persistence.md))。所有 `event.data` 都必须可序列化为 JSON;`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.events` 始终与后端可持久化的内容一致。新增会携带不可序列化数据、破坏核心执行嵌套或违反事件所有方声明关系的事件类型,都会构成磁盘格式的破坏性变更。 消费此契约的后端见 [persistence.md](persistence.md)。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.sessions` — `SessionStore` + +In-memory session store (`ctx.sessions`). + +Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose. + +```ts cordis-catalog +/** + * Create a session owned by the calling fiber: disposing that fiber stops + * event notification and removes the session from the store. `options.seed` + * populates the session with a copy of those events (replay/fork); + * `options.meta` attaches creation metadata (validated absolute `cwd`, seed + * and parent lineage, and delegation depth) as the immutable + * {@link SessionHeader} (the store fills `version`/`id`/`createdAt`). + * + * For an agent whose session must be torn down IN ORDER with its loop (so the + * loop's final events are published before the store attachment ends), do NOT use this + * — fold the session lifecycle into the agent's own effect via + * {@link prepare} + {@link enter} + {@link announce} (see + * `dsh-agent-loop`'s creation transaction). + * + * @param id - the session id; omitted, the store mints `session-`. + * @param options - seed events and/or creation metadata for the header. + * @returns the live session, already entered and announced. + * @throws if a session with `id` already exists, metadata is not a plain + * lossless-JSON record with valid scalar fields, or `meta.cwd` is a + * non-absolute path (storage backends key directories off it). + */ +create(id?: SessionId, options?: CreateSessionOptions): Session + +/** + * Build a session WITHOUT entering it into the store — validate the id/cwd and + * construct the {@link Session} (with its immutable {@link SessionHeader}). + * Pairs with {@link enter} + {@link announce}: a caller that owns a composite + * `ctx.effect` (the agent factory) folds the session lifecycle into that ONE + * effect so a fiber unload tears the session + agent down as a single ORDERED + * chain rather than as racing sibling effects — which would remove the publication hooks + * before the driver's closing events commit, dropping them. + * + * @param id - the session id; omitted, the store mints `session-`. + * @param options - seed events and/or creation metadata for the header. With + * `seedSource: 'persistence'`, metadata and events must be fresh detached + * graphs whose ownership transfers to this call: they are validated and + * frozen in place through {@link Session.fromRestore}, so the caller must + * retain no mutable aliases. + * @returns the constructed session, NOT yet in the store. + * @throws if a session with `id` already exists, metadata is not a plain + * lossless-JSON record with valid scalar fields, or `meta.cwd` is a + * non-absolute path. + */ +prepare(id?: SessionId, options?: PrepareSessionOptions): Session + +/** + * Enter a {@link prepare}d session into the store: install the module-private + * append publication hooks and add it to the store. Returns the DETACH + * disposer (hooks + store removal). Does NOT emit `session/created` — + * the caller yields this disposer inside its effect and THEN calls + * {@link announce}, so a throwing `session/created` listener rolls the attach + * back instead of leaking it. + * + * Re-checks the id for a duplicate: `prepare` and `enter` are public + * cross-package primitives and a caller may interleave arbitrary work (or + * another create) between them, so a stale prepared session must NOT overwrite + * a live store entry of the same id — its detach disposer would later delete + * the REAL session. The {@link create} convenience and the agent factory call + * the two back-to-back so they never trip this, but the public seam cannot + * assume that. + * + * @param session - a {@link prepare}d session not yet in the store. + * @returns the detach disposer (publication hooks + store removal). When called from + * a synchronous `session/created` listener, removal and disposal wait until + * that creation dispatch unwinds. + * @throws if a session with this id is already in the store. + */ +enter(session: Session): () => void + +/** Emit `session/created` exactly once for an {@link enter}ed session (with + * the carrier {@link enter} captured). Separate from {@link enter} so the + * caller can yield the detach disposer first (rollback safety — see + * {@link enter}). + * @param session - the entered session to announce to listeners. + * @throws if the session is not live or its announcement already began, + * including a reentrant call from a creation listener. */ +announce(session: Session): void + +/** + * Dispatch the awaited `session/flush` durability checkpoint for `session`, + * with the carrier captured at {@link enter}. THE flush entry point: the + * store owns the carrier, so callers (the checkpoint policy's per-request + * barrier, goal-session's idle checkpoint, teardown drains, and consumers + * that flush themselves before reading storage) must come through here + * rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner, + * one spelling, and the scoped-dispatch invariant can pin it. + * @param session - the session whose buffered events must reach durable storage. + * @returns whether at least one durability listener participated, after every + * listener has settled successfully. + * @throws the first registered listener failure after every listener settles. + */ +async flush(session: Session): Promise + +/** + * Look up a live session. + * @param id - the session id to look up. + * @returns the session, or undefined when no live session has that id. + */ +get(id: SessionId): Session | undefined + +/** + * All live sessions, in creation order. + * @returns a fresh array; mutating it does not affect the store. + */ +list(): Session[] + +/** + * Create a live child session from a stable prefix of a live source. + * `boundary` is an inclusive source event seq; omitted means the source's + * current last event. The selected slice may end with a between-turn event + * but must not end inside an open turn. + * + * @param source - Live source session object or id. + * @param boundary - Inclusive source event seq to fork through; omitted means + * the source's current last event, and omitted on an empty source forks an + * empty child. + * @param childSessionId - Optional child session id; omitted delegates to + * `SessionStore`'s id policy. + * @returns The created live child session. + */ +fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session +``` + +Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md) + +Source: [`packages/core/session/src/index.ts:807`](../../packages/core/session/src/index.ts) + + + +### `session/*` events + + + +#### `session/created` — emit + +Creation announcement during session publication. A synchronous throw vetoes and rolls back with a paired disposal; detach requested during dispatch is deferred. A returned-promise rejection is logged but cannot retroactively veto this synchronous boundary. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only sessions entered through that agent's context. + +```ts cordis-catalog +/** + * Creation announcement during session publication. A synchronous throw vetoes and rolls + * back with a paired disposal; detach requested during dispatch is deferred. + * A returned-promise rejection is logged but cannot retroactively veto this + * synchronous boundary. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners + * receive only sessions entered through that agent's context. + * @param session - the session just entered and announced. + * @dshScopeScan unsupported + * @mode emit + */ +'session/created'(this: Scoped, session: Session): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/session/src/index.ts:74`](../../packages/core/session/src/index.ts) + + + +#### `session/disposed` — emit + +Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin. Listener failures are logged and contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. + +```ts cordis-catalog +/** + * Emitted once when an announced session leaves the store, including + * publication rollback, but never for an entry whose creation announcement + * did not begin. Listener failures are logged and contained. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. + * @param session - the session that is no longer live in the store. + * @dshScopeScan unsupported + * @mode emit + */ +'session/disposed'(this: Scoped, session: Session): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/session/src/index.ts:84`](../../packages/core/session/src/index.ts) + + + +#### `session/event` — emit + +Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context. + +```ts cordis-catalog +/** + * Post-commit, fire-and-forget append feed. The listener snapshot resolves + * before the log push, but callbacks run after it; observer failures are + * logged and contained without making the committed append fail. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners + * receive only events from sessions entered through that agent's context. + * @param session - the session whose log grew. + * @param event - the appended event, exactly as recorded. + * @dshScopeScan unsupported + * @mode emit + */ +'session/event'(this: Scoped, session: Session, event: SessionEvent): void +``` + +Types: [Scoped](scope.md) · [SessionEvent](core.md) + +Source: [`packages/core/session/src/index.ts:96`](../../packages/core/session/src/index.ts) + + + +#### `session/flush` — parallel + +Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. + +```ts cordis-catalog +/** + * Awaited parallel durability checkpoint: every listener runs and the + * caller awaits all of them, with no waterfall veto. Scope-filtered dispatch + * (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. + * @param session - the session whose buffered events must reach durable storage. + * @dshScopeScan unsupported + * @mode parallel + */ +'session/flush'(this: Scoped, session: Session): Promise | void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/session/src/index.ts:105`](../../packages/core/session/src/index.ts) + diff --git a/docs/subsystems/settings.i18n.yaml b/docs/subsystems/settings.i18n.yaml index f4518f4414..5c40419366 100644 --- a/docs/subsystems/settings.i18n.yaml +++ b/docs/subsystems/settings.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/settings.md -settings.md: bd01c1d28407af9cab26f624a054a010e25a3ddd -settings.zh.md: 1cb7f8b507f29f2b6876fd48b4c37284df235e53 +settings.md: 9256bf9436d2e77093fc8c6a3728b62fc4e8d67f +settings.zh.md: f6d662ebaf35eeb9d046353ea1087b0eb86e7068 diff --git a/docs/subsystems/settings.md b/docs/subsystems/settings.md index bd01c1d284..9256bf9436 100644 --- a/docs/subsystems/settings.md +++ b/docs/subsystems/settings.md @@ -160,3 +160,151 @@ Every committed change — an in-process write or an externally observed provide /** Origin of one committed settings change. */ type SettingsUpdateSource = 'update' | 'provider' ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.settings` — `Settings` (abstract seam) + +Abstract settings service. Providers implement raw-document storage (`load`/`persist`) and push external changes through Settings.publish; the base class owns namespace registration, resolution, validation, change detection, and the `settings/updated` commit event. + +```ts cordis-catalog +/** + * Prepare the provider's user-editable document for a native editor. File + * providers may materialize an absent document before returning its path; + * non-file providers return undefined. + * @returns the absolute local document path, or undefined for non-file storage. + */ +prepareDocument(): Promise + +/** + * Register a namespace schema and receive its owner scope. The registration + * is an effect on the calling plugin's fiber: disposing that fiber removes + * the namespace and its observers. An invalid stored section fails the + * registration itself — the earliest point where the schema can judge it. + * @param ns - unique namespace; duplicate registration fails loud. + * @param schema - schemastery schema resolving this namespace's value. + * @param options - composition `base` layer and effect timing. + * @returns the owner scope for reads, observation, and updates. + */ +register(ns: SettingsNamespace, schema: z, options?: SettingsRegisterOptions): SettingsScope + +/** + * Describe every registered namespace for configuration surfaces, including + * the composition `base` and raw user layers so a form can mark which fields + * the user overrode (presence in `user`) and what a reset returns to. + * @param options - redaction switch; wire surfaces must redact. + * @returns one descriptor per registered namespace, in registration order. + */ +describe(options?: SettingsDescribeOptions): SettingsDescriptor[] + +/** + * Read one registered namespace's resolved value. + * @param ns - the namespace to read. + * @returns the resolved value, or `undefined` while unregistered. + */ +get(ns: SettingsNamespace): unknown + +/** + * Merge a patch into one registered namespace's user layer, validate the + * resolved candidate, persist through the provider, then commit and emit. + * A validation failure rejects before anything is persisted. Writes to one + * namespace are serialized: concurrent updates apply in call order, each + * merging over the previous write's committed section. + * @param ns - the registered namespace to update. + * @param patch - plain-object patch over the user section. + * @param expectedRevision - the descriptor `revision` the caller read; a + * namespace that moved past it rejects with {@link SettingsConflictError}. + */ +async update(ns: SettingsNamespace, patch: object, expectedRevision?: number): Promise + +/** + * Replace one registered namespace's user section wholesale, validate, + * persist, then commit and emit. Keys absent from `section` fall back to the + * composition `base` and schema defaults — this is the removal/reset path a + * merge-only patch cannot express (`replace({})` re-inherits everything). + * @param ns - the registered namespace to replace. + * @param section - the complete next user section. + * @param expectedRevision - the descriptor `revision` the caller read; a + * namespace that moved past it rejects with {@link SettingsConflictError}. + */ +async replace(ns: SettingsNamespace, section: object, expectedRevision?: number): Promise + +/** + * Apply path-addressed edits to one registered namespace's user section, + * validate, persist, then commit and emit. The ops are applied to the + * section as it stands when the write reaches the front of the queue, so a + * caller never has to restate fields it did not touch — and, crucially, + * cannot delete fields it never saw. This is the write path for any caller + * holding a redacted view; `replace` remains the wholesale reset. + * @param ns - the registered namespace to edit. + * @param ops - ordered path edits; later ops observe earlier ones. + * @param expectedRevision - the descriptor `revision` the caller read; a + * namespace that moved past it rejects with {@link SettingsConflictError}. + */ +async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise +``` + +Source: [`packages/settings/settings/src/index.ts:387`](../../packages/settings/settings/src/index.ts) + + + +### `settings/*` events + + + +#### `settings/document-updated` — emit + +One registered namespace's RAW user section changed, whether or not the resolved value did. `settings/updated` is the consumer-facing event and stays deep-equal-gated; this one exists for configuration surfaces, which must learn that a field went from inherited to overridden (same resolved value, different meaning) and that their held revision is stale. Listener containment matches `settings/updated`. + +```ts cordis-catalog +/** + * One registered namespace's RAW user section changed, whether or not the + * resolved value did. `settings/updated` is the consumer-facing event and + * stays deep-equal-gated; this one exists for configuration surfaces, + * which must learn that a field went from inherited to overridden (same + * resolved value, different meaning) and that their held revision is + * stale. Listener containment matches `settings/updated`. + * @param ns - the namespace whose stored section changed. + * @param revision - the namespace's new revision. + * @mode emit + */ +'settings/document-updated'(ns: SettingsNamespace, revision: number): void +``` + +Source: [`packages/settings/settings/src/index.ts:170`](../../packages/settings/settings/src/index.ts) + + + +#### `settings/updated` — emit + +Committed change to one registered namespace's resolved value. Emitted after the provider persisted (for `update`) or published (`provider`) the change; never emitted when the resolved value is deep-equal. Listener failures are contained and logged — a sync throw and an async rejection alike — except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions. + +```ts cordis-catalog +/** + * Committed change to one registered namespace's resolved value. Emitted + * after the provider persisted (for `update`) or published (`provider`) + * the change; never emitted when the resolved value is deep-equal. + * Listener failures are contained and logged — a sync throw and an async + * rejection alike — except `INVARIANT`-coded failures, which rethrow + * after every listener ran; that rethrow reaches the emitter only from + * synchronous listeners, so invariant checks on this event must not be + * async functions. + * @param ns - the namespace whose resolved value changed. + * @param next - the new resolved value. + * @param prev - the previous resolved value. + * @param source - whether the change entered through `update()` or the provider. + * @mode emit + */ +'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void +``` + +Source: [`packages/settings/settings/src/index.ts:157`](../../packages/settings/settings/src/index.ts) + diff --git a/docs/subsystems/settings.zh.md b/docs/subsystems/settings.zh.md index 1cb7f8b507..f6d662ebaf 100644 --- a/docs/subsystems/settings.zh.md +++ b/docs/subsystems/settings.zh.md @@ -160,3 +160,151 @@ interface SettingsDescribeOptions { /** Origin of one committed settings change. */ type SettingsUpdateSource = 'update' | 'provider' ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.settings` — `Settings` (abstract seam) + +Abstract settings service. Providers implement raw-document storage (`load`/`persist`) and push external changes through Settings.publish; the base class owns namespace registration, resolution, validation, change detection, and the `settings/updated` commit event. + +```ts cordis-catalog +/** + * Prepare the provider's user-editable document for a native editor. File + * providers may materialize an absent document before returning its path; + * non-file providers return undefined. + * @returns the absolute local document path, or undefined for non-file storage. + */ +prepareDocument(): Promise + +/** + * Register a namespace schema and receive its owner scope. The registration + * is an effect on the calling plugin's fiber: disposing that fiber removes + * the namespace and its observers. An invalid stored section fails the + * registration itself — the earliest point where the schema can judge it. + * @param ns - unique namespace; duplicate registration fails loud. + * @param schema - schemastery schema resolving this namespace's value. + * @param options - composition `base` layer and effect timing. + * @returns the owner scope for reads, observation, and updates. + */ +register(ns: SettingsNamespace, schema: z, options?: SettingsRegisterOptions): SettingsScope + +/** + * Describe every registered namespace for configuration surfaces, including + * the composition `base` and raw user layers so a form can mark which fields + * the user overrode (presence in `user`) and what a reset returns to. + * @param options - redaction switch; wire surfaces must redact. + * @returns one descriptor per registered namespace, in registration order. + */ +describe(options?: SettingsDescribeOptions): SettingsDescriptor[] + +/** + * Read one registered namespace's resolved value. + * @param ns - the namespace to read. + * @returns the resolved value, or `undefined` while unregistered. + */ +get(ns: SettingsNamespace): unknown + +/** + * Merge a patch into one registered namespace's user layer, validate the + * resolved candidate, persist through the provider, then commit and emit. + * A validation failure rejects before anything is persisted. Writes to one + * namespace are serialized: concurrent updates apply in call order, each + * merging over the previous write's committed section. + * @param ns - the registered namespace to update. + * @param patch - plain-object patch over the user section. + * @param expectedRevision - the descriptor `revision` the caller read; a + * namespace that moved past it rejects with {@link SettingsConflictError}. + */ +async update(ns: SettingsNamespace, patch: object, expectedRevision?: number): Promise + +/** + * Replace one registered namespace's user section wholesale, validate, + * persist, then commit and emit. Keys absent from `section` fall back to the + * composition `base` and schema defaults — this is the removal/reset path a + * merge-only patch cannot express (`replace({})` re-inherits everything). + * @param ns - the registered namespace to replace. + * @param section - the complete next user section. + * @param expectedRevision - the descriptor `revision` the caller read; a + * namespace that moved past it rejects with {@link SettingsConflictError}. + */ +async replace(ns: SettingsNamespace, section: object, expectedRevision?: number): Promise + +/** + * Apply path-addressed edits to one registered namespace's user section, + * validate, persist, then commit and emit. The ops are applied to the + * section as it stands when the write reaches the front of the queue, so a + * caller never has to restate fields it did not touch — and, crucially, + * cannot delete fields it never saw. This is the write path for any caller + * holding a redacted view; `replace` remains the wholesale reset. + * @param ns - the registered namespace to edit. + * @param ops - ordered path edits; later ops observe earlier ones. + * @param expectedRevision - the descriptor `revision` the caller read; a + * namespace that moved past it rejects with {@link SettingsConflictError}. + */ +async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise +``` + +Source: [`packages/settings/settings/src/index.ts:387`](../../packages/settings/settings/src/index.ts) + + + +### `settings/*` events + + + +#### `settings/document-updated` — emit + +One registered namespace's RAW user section changed, whether or not the resolved value did. `settings/updated` is the consumer-facing event and stays deep-equal-gated; this one exists for configuration surfaces, which must learn that a field went from inherited to overridden (same resolved value, different meaning) and that their held revision is stale. Listener containment matches `settings/updated`. + +```ts cordis-catalog +/** + * One registered namespace's RAW user section changed, whether or not the + * resolved value did. `settings/updated` is the consumer-facing event and + * stays deep-equal-gated; this one exists for configuration surfaces, + * which must learn that a field went from inherited to overridden (same + * resolved value, different meaning) and that their held revision is + * stale. Listener containment matches `settings/updated`. + * @param ns - the namespace whose stored section changed. + * @param revision - the namespace's new revision. + * @mode emit + */ +'settings/document-updated'(ns: SettingsNamespace, revision: number): void +``` + +Source: [`packages/settings/settings/src/index.ts:170`](../../packages/settings/settings/src/index.ts) + + + +#### `settings/updated` — emit + +Committed change to one registered namespace's resolved value. Emitted after the provider persisted (for `update`) or published (`provider`) the change; never emitted when the resolved value is deep-equal. Listener failures are contained and logged — a sync throw and an async rejection alike — except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions. + +```ts cordis-catalog +/** + * Committed change to one registered namespace's resolved value. Emitted + * after the provider persisted (for `update`) or published (`provider`) + * the change; never emitted when the resolved value is deep-equal. + * Listener failures are contained and logged — a sync throw and an async + * rejection alike — except `INVARIANT`-coded failures, which rethrow + * after every listener ran; that rethrow reaches the emitter only from + * synchronous listeners, so invariant checks on this event must not be + * async functions. + * @param ns - the namespace whose resolved value changed. + * @param next - the new resolved value. + * @param prev - the previous resolved value. + * @param source - whether the change entered through `update()` or the provider. + * @mode emit + */ +'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void +``` + +Source: [`packages/settings/settings/src/index.ts:157`](../../packages/settings/settings/src/index.ts) + diff --git a/docs/subsystems/skills.i18n.yaml b/docs/subsystems/skills.i18n.yaml index 6ab9693cca..84628f50b7 100644 --- a/docs/subsystems/skills.i18n.yaml +++ b/docs/subsystems/skills.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/skills.md -skills.md: d862cbc07135680c2377b4c8c82c80d055344221 -skills.zh.md: 3f8c034ec2aa24b4acdbbc1e2ac27717c21649d5 +skills.md: 78d510fafca0017b9528a215270172a74e9b0de1 +skills.zh.md: e88c9f1ba190eb1e90966d35c7dcc2eb86ecd6cc diff --git a/docs/subsystems/skills.md b/docs/subsystems/skills.md index d862cbc071..78d510fafc 100644 --- a/docs/subsystems/skills.md +++ b/docs/subsystems/skills.md @@ -218,3 +218,93 @@ interface Config { Before each later model step, the consumer applies exact tool visibility and digests the exact rendered entries between the `` tags from a complete snapshot. It derives the comparison baseline from the same entries in the newest recognizable visible catalog message sourced by the plugin. A changed digest appends a durable full replacement through `agent.inject()`; deleting every skill appends an explicit empty replacement. Incomplete snapshots preserve the last-good model view. If compaction hides every historical catalog message, the next complete snapshot re-establishes the current catalog; an empty view with no prior catalog emits nothing. These catalog messages are session history, not World State. The model-facing `skill({ name })` tool validates the kebab-case name, finds the summary in the invocation-neutral catalog, rejects it before loading unless `isModelInvocable` permits access, then rereads the complete definition for the calling agent cwd and rechecks the policy before returning content. It reports an unresolved skill as unknown or no longer available and returns a tool result containing ``, ``, and ``. `resourceBase` resolves explicitly referenced scripts, references, and assets only as needed; the loaded result does not enumerate a skill directory. Body-only edits therefore change later tool calls without producing catalog messages or rewriting earlier tool results. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.skills` — `SkillService` + +Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted invocation-neutral summaries, and loads full skill bodies on demand. + +```ts cordis-catalog +/** + * Register a borrowed same-process provider synchronously during plugin apply. Duplicate and + * reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters + * the provider and invalidates catalog caches. + * @param create - synchronous factory receiving this registration's lifecycle and invalidation control. + * @returns the exact Cordis effect disposer that unregisters this provider; + * composite effects may yield it directly to preserve teardown ordering. + */ +registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void + +/** + * Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which + * outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and + * receives a no-op disposer so it cannot remove the winner. + * @param skill - the skill definition input; omitted invocation and provider fields receive defaults. + * @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches. + */ +register(skill: SkillRegistration): () => void + +/** + * List invocation-neutral skill summaries for a workspace. Consumers apply + * model or user invocation policy at their operational boundary. Lookup + * options and provider candidates are readonly same-process values borrowed + * throughout discovery. + * @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery. + * @returns all sorted winning summaries. + */ +async list(options: SkillLookupOptions = {}): Promise + +/** + * Observe the current invocation-neutral catalog and whether discovery completed within a stable revision. + * Incomplete observations are never cached, allowing consumers to retain last-good state and + * retry on their next request boundary. + * @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery. + * @returns sorted summaries plus discovery-completeness state. + */ +async snapshot(options: SkillLookupOptions = {}): Promise + +/** + * Load and validate the winning candidate, passing its opaque discovery locator back to the + * provider. Cancellation is rechecked after selection, including cache hits, and raced against + * loading so an uncooperative provider cannot hang the caller. + * @param name - kebab-case skill name. + * @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work. + * @returns the full skill, including body content, or `undefined`. + */ +async get(name: string, options: SkillLookupOptions = {}): Promise +``` + +Source: [`packages/skill/skill/src/index.ts:304`](../../packages/skill/skill/src/index.ts) + + + +### `skills/*` events + + + +#### `skills/change` — emit + +A skill provider, runtime contribution, or provider-backed catalog may have changed. This is an unfiltered invalidation notification; consumers refetch the catalog for their own lookup options. Listener failures are contained and cannot veto the registry mutation. + +```ts cordis-catalog +/** + * A skill provider, runtime contribution, or provider-backed catalog may + * have changed. This is an unfiltered invalidation notification; consumers + * refetch the catalog for their own lookup options. Listener failures are + * contained and cannot veto the registry mutation. + * @mode emit + */ +'skills/change'(): void +``` + +Source: [`packages/skill/skill/src/index.ts:283`](../../packages/skill/skill/src/index.ts) + diff --git a/docs/subsystems/skills.zh.md b/docs/subsystems/skills.zh.md index 3f8c034ec2..e88c9f1ba1 100644 --- a/docs/subsystems/skills.zh.md +++ b/docs/subsystems/skills.zh.md @@ -218,3 +218,93 @@ interface Config { 在后续每个模型步骤之前,消费方都会应用精确的工具可见性,并对完整快照中 `` 标签之间精确渲染的条目计算 digest。它以该插件所发布、最新一条可识别且仍可见的目录消息中的相同条目作为比较基线。digest 发生变化时,会通过 `agent.inject()` 追加一条持久的完整目录替换;删除所有 skill 时会追加一条显式的空替换。不完整快照会保留上一份可用模型视图。如果压缩(compaction)隐藏了所有历史目录消息,下一份完整快照会重新建立当前目录;如果视图为空且从未发布目录,则不发送任何内容。这些目录消息属于会话历史,而非 World State。 面向模型的 `skill({ name })` 工具校验 kebab-case 名称,在与调用策略无关的目录中查找摘要,并在加载前通过 `isModelInvocable` 拒绝无权访问的 skill;随后它根据调用方 agent 的 cwd 重新读取完整定义,并在返回内容前再次检查策略。该工具将无法解析的 skill 报告为未知或已不可用,并返回包含 ``、`` 和 `` 的工具结果。`resourceBase` 仅按需解析显式引用的脚本、参考资料和资产;加载结果不枚举 skill 目录。因此,仅修改正文会改变后续工具调用,而不会生成目录消息或改写先前工具结果。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.skills` — `SkillService` + +Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted invocation-neutral summaries, and loads full skill bodies on demand. + +```ts cordis-catalog +/** + * Register a borrowed same-process provider synchronously during plugin apply. Duplicate and + * reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters + * the provider and invalidates catalog caches. + * @param create - synchronous factory receiving this registration's lifecycle and invalidation control. + * @returns the exact Cordis effect disposer that unregisters this provider; + * composite effects may yield it directly to preserve teardown ordering. + */ +registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void + +/** + * Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which + * outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and + * receives a no-op disposer so it cannot remove the winner. + * @param skill - the skill definition input; omitted invocation and provider fields receive defaults. + * @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches. + */ +register(skill: SkillRegistration): () => void + +/** + * List invocation-neutral skill summaries for a workspace. Consumers apply + * model or user invocation policy at their operational boundary. Lookup + * options and provider candidates are readonly same-process values borrowed + * throughout discovery. + * @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery. + * @returns all sorted winning summaries. + */ +async list(options: SkillLookupOptions = {}): Promise + +/** + * Observe the current invocation-neutral catalog and whether discovery completed within a stable revision. + * Incomplete observations are never cached, allowing consumers to retain last-good state and + * retry on their next request boundary. + * @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery. + * @returns sorted summaries plus discovery-completeness state. + */ +async snapshot(options: SkillLookupOptions = {}): Promise + +/** + * Load and validate the winning candidate, passing its opaque discovery locator back to the + * provider. Cancellation is rechecked after selection, including cache hits, and raced against + * loading so an uncooperative provider cannot hang the caller. + * @param name - kebab-case skill name. + * @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work. + * @returns the full skill, including body content, or `undefined`. + */ +async get(name: string, options: SkillLookupOptions = {}): Promise +``` + +Source: [`packages/skill/skill/src/index.ts:304`](../../packages/skill/skill/src/index.ts) + + + +### `skills/*` events + + + +#### `skills/change` — emit + +A skill provider, runtime contribution, or provider-backed catalog may have changed. This is an unfiltered invalidation notification; consumers refetch the catalog for their own lookup options. Listener failures are contained and cannot veto the registry mutation. + +```ts cordis-catalog +/** + * A skill provider, runtime contribution, or provider-backed catalog may + * have changed. This is an unfiltered invalidation notification; consumers + * refetch the catalog for their own lookup options. Listener failures are + * contained and cannot veto the registry mutation. + * @mode emit + */ +'skills/change'(): void +``` + +Source: [`packages/skill/skill/src/index.ts:283`](../../packages/skill/skill/src/index.ts) + diff --git a/docs/subsystems/spill.i18n.yaml b/docs/subsystems/spill.i18n.yaml index 17f210bc85..67b938a93a 100644 --- a/docs/subsystems/spill.i18n.yaml +++ b/docs/subsystems/spill.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/spill.md -spill.md: a798d8143b2849dc0cf49d04e7019ce796cdee45 -spill.zh.md: 1167c6f985dbc204dc7166b0fb5854dcf55bc72f +# pnpm run verify-translation-pairing --write docs/subsystems/spill.md +spill.md: 41a3fdaf7d9b15fbc6d479724f77f579b8a98738 +spill.zh.md: 02a6fc4e88dfcfdb47f7639d988fc09c6bc42c69 diff --git a/docs/subsystems/spill.md b/docs/subsystems/spill.md index a798d8143b..41a3fdaf7d 100644 --- a/docs/subsystems/spill.md +++ b/docs/subsystems/spill.md @@ -83,3 +83,35 @@ type SpillLocator = Branded<'SpillLocator'> `SpillStore` (`ctx.spillStore`, defined in [`packages/spill/spill/src/index.ts`](../../packages/spill/spill/src/index.ts)) is a one-method abstract service: `saveText(input) → Promise`. It persists the FULL `content` and REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable). The seam owns storage only: no retention policy, no tool-result replacement, no retrieval/search API. The local backend ([dsh-spill-local](../../packages/spill/spill-local)) writes under `/session-/-` — a configured or lazily-created private (0700) root, a `sha256(sessionId)` session subdir, and an exclusive owner-only (`open(path, 'wx', 0o600)`) write so a planted symlink cannot redirect it. Its `locator` is the local path and its `retrievalHint` tells the model to use `read` or `grep` on that path. The policy consumer ([dsh-spill-policy](../../packages/spill/spill-policy)) replaces an over-`maxInlineBytes` plain-text final result with a retention-library head/tail preview plus the spill reference, best-effort: a save failure keeps the original inline result rather than turning a successful call into an `isError`. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.spillStore` — `SpillStore` (abstract seam) + +Abstract spill storage service. Subclass, implement saveText, and load the subclass as a plugin — it registers as `ctx.spillStore` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior). + +Semantics every implementation must honor: + +- saveText persists the FULL `content` verbatim and returns an opaque locator, exact byte length, and model-facing retrieval guidance. +- Storage is scoped by the request's SaveTextSpill.owner session; the backend chooses a private (not world-readable) location and a collision-free name derived from — never equal to — the caller's `suggestedName`. +- `saveText` REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable); the caller decides how to degrade (the spill policy treats a rejection as best-effort and keeps the inline result). + +```ts cordis-catalog +/** + * Persist `input.content` to a session-scoped spill artifact. + * @param input - the owner, provenance, suggested name, and full text to save. + * @returns the saved artifact's {@link SpillRef}; rejects on a storage failure. + */ +abstract saveText(input: SaveTextSpill): Promise +``` + +Source: [`packages/spill/spill/src/index.ts:45`](../../packages/spill/spill/src/index.ts) + diff --git a/docs/subsystems/spill.zh.md b/docs/subsystems/spill.zh.md index 1167c6f985..02a6fc4e88 100644 --- a/docs/subsystems/spill.zh.md +++ b/docs/subsystems/spill.zh.md @@ -83,3 +83,35 @@ type SpillLocator = Branded<'SpillLocator'> `SpillStore`(`ctx.spillStore`,定义于 [`packages/spill/spill/src/index.ts`](../../packages/spill/spill/src/index.ts))是只有一个方法的抽象服务:`saveText(input) → Promise`。它持久保存完整的 `content`,并在实际存储失败(权限、ENOSPC、后端不可用)时拒绝。该 seam 只负责存储:不负责保留策略、工具结果替换或检索/搜索 API。 本地后端([dsh-spill-local](../../packages/spill/spill-local))写入 `/session-/-`:根目录是已配置或延迟创建的私有(0700)目录,会话子目录采用 `sha256(sessionId)`,并通过排他的仅所有者可访问写入(`open(path, 'wx', 0o600)`)防止预先植入的符号链接重定向写入。其 `locator` 是本地路径,`retrievalHint` 则告知模型在该路径上使用 `read` 或 `grep`。策略消费方([dsh-spill-policy](../../packages/spill/spill-policy))会把超过 `maxInlineBytes` 的纯文本最终结果替换为保留库生成的首尾预览和落盘引用;该过程尽力而为:保存失败时保留原始内联结果,而不会把成功的调用变成 `isError`。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.spillStore` — `SpillStore` (abstract seam) + +Abstract spill storage service. Subclass, implement saveText, and load the subclass as a plugin — it registers as `ctx.spillStore` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior). + +Semantics every implementation must honor: + +- saveText persists the FULL `content` verbatim and returns an opaque locator, exact byte length, and model-facing retrieval guidance. +- Storage is scoped by the request's SaveTextSpill.owner session; the backend chooses a private (not world-readable) location and a collision-free name derived from — never equal to — the caller's `suggestedName`. +- `saveText` REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable); the caller decides how to degrade (the spill policy treats a rejection as best-effort and keeps the inline result). + +```ts cordis-catalog +/** + * Persist `input.content` to a session-scoped spill artifact. + * @param input - the owner, provenance, suggested name, and full text to save. + * @returns the saved artifact's {@link SpillRef}; rejects on a storage failure. + */ +abstract saveText(input: SaveTextSpill): Promise +``` + +Source: [`packages/spill/spill/src/index.ts:45`](../../packages/spill/spill/src/index.ts) + diff --git a/docs/subsystems/storage.i18n.yaml b/docs/subsystems/storage.i18n.yaml index 6560eed831..98092af5b3 100644 --- a/docs/subsystems/storage.i18n.yaml +++ b/docs/subsystems/storage.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/storage.md -storage.md: 5d54d7337b5ef52e24179758130b9fa8cd196376 -storage.zh.md: 46346b64351322d40de166a71f7a3204c8902b2d +storage.md: 52dc84b699921e911276ce5214bc975abfd22fa4 +storage.zh.md: f614b77153e835264e1650515946a9d2acf70e57 diff --git a/docs/subsystems/storage.md b/docs/subsystems/storage.md index 5d54d7337b..52dc84b699 100644 --- a/docs/subsystems/storage.md +++ b/docs/subsystems/storage.md @@ -8,7 +8,7 @@ Source: [`packages/storage/storage/src/backend.ts`](../../packages/storage/stora ## The hub: `ctx.storage` -`Storage` ([signatures](../cordis-catalog/services.md#ctxstorage--storage)) is a meeting point, not a store. `ctx.storage.backend` is a name → backend table: multiple backends stay mounted side by side, and which backend serves which consumer is that consumer's configuration (the domain layer's route table), never a hub-global choice. `register(name, backend)` returns the disposer; duplicate names and unknown lookups throw `StorageError`. Disposal only unregisters the name — the owning plugin closes the backend after unregistering. Each backend plugin also publishes a lifecycle-only service key (`storageBackendServiceKey(name)`), which form providers inject so their activation cannot race backend registration. +`Storage` ([signatures](#ctxstorage--storage)) is a meeting point, not a store. `ctx.storage.backend` is a name → backend table: multiple backends stay mounted side by side, and which backend serves which consumer is that consumer's configuration (the domain layer's route table), never a hub-global choice. `register(name, backend)` returns the disposer; duplicate names and unknown lookups throw `StorageError`. Disposal only unregisters the name — the owning plugin closes the backend after unregistering. Each backend plugin also publishes a lifecycle-only service key (`storageBackendServiceKey(name)`), which form providers inject so their activation cannot race backend registration. Data forms mount on the hub under a merge-extensible key map: @@ -99,11 +99,11 @@ Reads are synchronous from authoritative in-memory state: `KvTable` exposes `get ## The domain facility: `ctx.storageDomain` -`DomainFacility` ([signatures](../cordis-catalog/services.md#ctxstoragedomain--domainfacility)) opens declared domains over routed backends. Routing is the domain plugin's configuration, never the hub's: `backend` names the required default route and `routes` overrides it per domain name. `open(spec)` runs a strict sequence, each step failing the whole call: it rejects a name already open or still closing (`already-open`), resolves the route (`backend-not-found`), requires the backend's `kv` facet (`facet-unsupported`), opens the unit (backend `version-mismatch`/`malformed-medium` pass through), and validates every stored record and global against the spec's zod schemas (`invalid-record` with the offending table and key). The caller owns the returned handle and releases it with `Domain.close()`; domains still open when the plugin unmounts are closed by the facility, and a closed domain's name frees for reopening only after teardown fully completes. `get(name)` is an untyped diagnostic lookup onto the package-private `DomainImpl` runtime behind every typed handle; `closeAll()` is the unmount path. +`DomainFacility` ([signatures](#ctxstoragedomain--domainfacility)) opens declared domains over routed backends. Routing is the domain plugin's configuration, never the hub's: `backend` names the required default route and `routes` overrides it per domain name. `open(spec)` runs a strict sequence, each step failing the whole call: it rejects a name already open or still closing (`already-open`), resolves the route (`backend-not-found`), requires the backend's `kv` facet (`facet-unsupported`), opens the unit (backend `version-mismatch`/`malformed-medium` pass through), and validates every stored record and global against the spec's zod schemas (`invalid-record` with the offending table and key). The caller owns the returned handle and releases it with `Domain.close()`; domains still open when the plugin unmounts are closed by the facility, and a closed domain's name frees for reopening only after teardown fully completes. `get(name)` is an untyped diagnostic lookup onto the package-private `DomainImpl` runtime behind every typed handle; `closeAll()` is the unmount path. ## The change event: `domain/changed` -Every durable write emits one event strictly after the backend acknowledged durability, in the domain's write-chain order ([event entry](../cordis-catalog/events.md#domainchanged--emit)): +Every durable write emits one event strictly after the backend acknowledged durability, in the domain's write-chain order ([event entry](#domainchanged--emit)): ```ts type-equiv /** Shared location fields of one durable domain change. */ @@ -123,3 +123,107 @@ type DomainChanged = DomainChangedPut | DomainChangedDeleted ``` `put` (inserts, overwrites, and global writes) carries the new snapshot in `value` — never the old value; a diffing consumer keeps its own previous snapshot. `deleted` is a tombstone with no value. The event is a notification, not a transaction participant: the commit point has passed at emission, so a synchronously throwing listener is contained with a logged warning rather than rejecting the already-durable write, and emitted values equal the in-memory state at emission. The event is in-process only; cross-process change push is deferred work recorded in the [package README](../../packages/storage/storage-domain/README.md). + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.storage` — `Storage` + +The storage hub service. Backends register under `backend`; data forms mount under their `StorageForms` key and are reached as `ctx.storage.`. + +```ts cordis-catalog +/** + * Mount a data-form facility on the hub. Mounting is an effect: the + * returned disposer unmounts the form. + * @param form - Form key declared in {@link StorageForms}. + * @param facility - The facility instance to expose. + * @returns the disposer that unmounts the form. + */ +mount(form: K, facility: StorageForms[K]): () => void + +/** + * Resolve a mounted data form. + * @param form - Form key declared in {@link StorageForms}. + * @returns the mounted facility. + */ +form(form: K): StorageForms[K] +``` + +Source: [`packages/storage/storage/src/index.ts:47`](../../packages/storage/storage/src/index.ts) + + + +### `ctx.storageDomain` — `DomainFacility` + +The mounted domain facility. Opens declared domains over routed backends; one facility instance owns the open-domain table and enforces single-open per domain name. + +```ts cordis-catalog +/** + * Open one declared domain. Steps, each failing the whole call: reject a + * name that is already open (`already-open`); resolve the backend route + * (`backend-not-found` passes through from the hub); require its `kv` facet + * (`facet-unsupported`); open the unit projected from the spec (backend + * `version-mismatch`/`malformed-medium` pass through); load and validate + * every stored record against the spec's zod schemas (`invalid-record` + * with the offending table and key); construct the domain. + * + * Lifecycle: the CALLER owns the returned handle and closes it via + * `Domain.close()` (typically as its own `ctx.effect` disposer) — the + * facility does not tie the domain to any consumer fiber. Domains still + * open when the facility unmounts are closed by the plugin disposer. + * @param spec - The domain declaration, typically from `defineDomain`. + * @returns the opened domain handle, typed by the spec. + */ +async open(spec: S): Promise> + +/** + * Look up an open domain by name, untyped. Diagnostic surface (the package + * invariant cross-checks change events against live domain state); typed + * consumers hold the handle returned by {@link open}. + * @param name - Domain name. + * @returns the open domain runtime, or `undefined` when not open. + */ +get(name: string): DomainImpl | undefined + +/** + * Close every domain still open on this facility. The unmount path for + * consumers that never called `Domain.close()` themselves; closing is + * idempotent, so double-closing an already-closed domain is harmless. + * @returns resolution after every unit is released. + */ +async closeAll(): Promise +``` + +Source: [`packages/storage/storage-domain/src/index.ts:69`](../../packages/storage/storage-domain/src/index.ts) + + + +### `domain/*` events + + + +#### `domain/changed` — emit + +A domain record or the global singleton changed, emitted once per write strictly after the backend acknowledged durability. Events of one domain arrive in its write-chain order. + +```ts cordis-catalog +/** + * A domain record or the global singleton changed, emitted once per write + * strictly after the backend acknowledged durability. Events of one + * domain arrive in its write-chain order. + * @param change - domain, table (`''` for global), key (`''` for global), + * operation discriminant, and on `put` the new snapshot. + * @mode emit + */ +'domain/changed'(change: DomainChanged): void +``` + +Source: [`packages/storage/storage-domain/src/events.ts:46`](../../packages/storage/storage-domain/src/events.ts) + diff --git a/docs/subsystems/storage.zh.md b/docs/subsystems/storage.zh.md index 46346b6435..f614b77153 100644 --- a/docs/subsystems/storage.zh.md +++ b/docs/subsystems/storage.zh.md @@ -8,7 +8,7 @@ ## 枢纽:`ctx.storage` -`Storage`([签名](../cordis-catalog/services.md#ctxstorage--storage))是汇合点,不是存储本体。`ctx.storage.backend` 是一张名称 → 后端的表:多个后端并排保持挂载,哪个后端服务哪个消费方由该消费方自己的配置决定(即领域层的路由表),绝不是枢纽全局的选择。`register(name, backend)` 返回 disposer;重复名称与查找未知名称都抛出 `StorageError`。dispose(资源释放)只注销名称——由拥有插件在注销之后自行关闭后端。每个后端插件还会发布一个仅用于生命周期的服务键(`storageBackendServiceKey(name)`),数据形式提供方注入它,使自身激活不会与后端注册发生竞态。 +`Storage`([签名](#ctxstorage--storage))是汇合点,不是存储本体。`ctx.storage.backend` 是一张名称 → 后端的表:多个后端并排保持挂载,哪个后端服务哪个消费方由该消费方自己的配置决定(即领域层的路由表),绝不是枢纽全局的选择。`register(name, backend)` 返回 disposer;重复名称与查找未知名称都抛出 `StorageError`。dispose(资源释放)只注销名称——由拥有插件在注销之后自行关闭后端。每个后端插件还会发布一个仅用于生命周期的服务键(`storageBackendServiceKey(name)`),数据形式提供方注入它,使自身激活不会与后端注册发生竞态。 数据形式以一张可合并扩展的键 map 挂载到枢纽上: @@ -99,11 +99,11 @@ interface Domain { ## 领域 facility:`ctx.storageDomain` -`DomainFacility`([签名](../cordis-catalog/services.md#ctxstoragedomain--domainfacility))在经过路由的后端之上打开已声明的领域。路由是领域插件的配置,绝不属于枢纽:`backend` 指定必填的默认路由,`routes` 按领域名逐个覆盖。`open(spec)` 按严格顺序执行,每一步失败都使整个调用失败:拒绝已打开或仍在关闭中的名称(`already-open`),解析路由(`backend-not-found`),要求后端具备 `kv` facet(`facet-unsupported`),打开 unit(后端的 `version-mismatch`/`malformed-medium` 原样透传),并按 spec 的 zod schema 校验每条已存储记录和 global(`invalid-record`,附带出错的表与键)。调用方拥有返回的句柄,并用 `Domain.close()` 释放它;插件卸载时仍处于打开状态的领域由 facility 负责关闭,已关闭领域的名称只有在拆除完全结束后才释放出来供重新打开。`get(name)` 是无类型的诊断查找,命中的是每个类型化句柄背后包内私有的 `DomainImpl` 运行时;`closeAll()` 是卸载路径。 +`DomainFacility`([签名](#ctxstoragedomain--domainfacility))在经过路由的后端之上打开已声明的领域。路由是领域插件的配置,绝不属于枢纽:`backend` 指定必填的默认路由,`routes` 按领域名逐个覆盖。`open(spec)` 按严格顺序执行,每一步失败都使整个调用失败:拒绝已打开或仍在关闭中的名称(`already-open`),解析路由(`backend-not-found`),要求后端具备 `kv` facet(`facet-unsupported`),打开 unit(后端的 `version-mismatch`/`malformed-medium` 原样透传),并按 spec 的 zod schema 校验每条已存储记录和 global(`invalid-record`,附带出错的表与键)。调用方拥有返回的句柄,并用 `Domain.close()` 释放它;插件卸载时仍处于打开状态的领域由 facility 负责关闭,已关闭领域的名称只有在拆除完全结束后才释放出来供重新打开。`get(name)` 是无类型的诊断查找,命中的是每个类型化句柄背后包内私有的 `DomainImpl` 运行时;`closeAll()` 是卸载路径。 ## 变更事件:`domain/changed` -每次持久写入都发出一个事件,严格发生在后端确认持久性之后,顺序遵循该领域的写链([事件条目](../cordis-catalog/events.md#domainchanged--emit)): +每次持久写入都发出一个事件,严格发生在后端确认持久性之后,顺序遵循该领域的写链([事件条目](#domainchanged--emit)): ```ts type-equiv /** Shared location fields of one durable domain change. */ @@ -123,3 +123,107 @@ type DomainChanged = DomainChangedPut | DomainChangedDeleted ``` `put`(插入、覆写和 global 写入)在 `value` 中携带新快照——绝不携带旧值;需要做差异比较的消费方自行保留上一份快照。`deleted` 是不携带值的墓碑。该事件是通知,不是事务参与者:发出时提交点已经过去,因此同步抛出的监听器会被兜住并记录一条警告,而不会让已经持久的写入被拒绝;发出的值等于发出时刻的内存态。该事件仅限进程内;跨进程的变更推送是延后工作,记录在[包 README](../../packages/storage/storage-domain/README.md)中。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.storage` — `Storage` + +The storage hub service. Backends register under `backend`; data forms mount under their `StorageForms` key and are reached as `ctx.storage.`. + +```ts cordis-catalog +/** + * Mount a data-form facility on the hub. Mounting is an effect: the + * returned disposer unmounts the form. + * @param form - Form key declared in {@link StorageForms}. + * @param facility - The facility instance to expose. + * @returns the disposer that unmounts the form. + */ +mount(form: K, facility: StorageForms[K]): () => void + +/** + * Resolve a mounted data form. + * @param form - Form key declared in {@link StorageForms}. + * @returns the mounted facility. + */ +form(form: K): StorageForms[K] +``` + +Source: [`packages/storage/storage/src/index.ts:47`](../../packages/storage/storage/src/index.ts) + + + +### `ctx.storageDomain` — `DomainFacility` + +The mounted domain facility. Opens declared domains over routed backends; one facility instance owns the open-domain table and enforces single-open per domain name. + +```ts cordis-catalog +/** + * Open one declared domain. Steps, each failing the whole call: reject a + * name that is already open (`already-open`); resolve the backend route + * (`backend-not-found` passes through from the hub); require its `kv` facet + * (`facet-unsupported`); open the unit projected from the spec (backend + * `version-mismatch`/`malformed-medium` pass through); load and validate + * every stored record against the spec's zod schemas (`invalid-record` + * with the offending table and key); construct the domain. + * + * Lifecycle: the CALLER owns the returned handle and closes it via + * `Domain.close()` (typically as its own `ctx.effect` disposer) — the + * facility does not tie the domain to any consumer fiber. Domains still + * open when the facility unmounts are closed by the plugin disposer. + * @param spec - The domain declaration, typically from `defineDomain`. + * @returns the opened domain handle, typed by the spec. + */ +async open(spec: S): Promise> + +/** + * Look up an open domain by name, untyped. Diagnostic surface (the package + * invariant cross-checks change events against live domain state); typed + * consumers hold the handle returned by {@link open}. + * @param name - Domain name. + * @returns the open domain runtime, or `undefined` when not open. + */ +get(name: string): DomainImpl | undefined + +/** + * Close every domain still open on this facility. The unmount path for + * consumers that never called `Domain.close()` themselves; closing is + * idempotent, so double-closing an already-closed domain is harmless. + * @returns resolution after every unit is released. + */ +async closeAll(): Promise +``` + +Source: [`packages/storage/storage-domain/src/index.ts:69`](../../packages/storage/storage-domain/src/index.ts) + + + +### `domain/*` events + + + +#### `domain/changed` — emit + +A domain record or the global singleton changed, emitted once per write strictly after the backend acknowledged durability. Events of one domain arrive in its write-chain order. + +```ts cordis-catalog +/** + * A domain record or the global singleton changed, emitted once per write + * strictly after the backend acknowledged durability. Events of one + * domain arrive in its write-chain order. + * @param change - domain, table (`''` for global), key (`''` for global), + * operation discriminant, and on `put` the new snapshot. + * @mode emit + */ +'domain/changed'(change: DomainChanged): void +``` + +Source: [`packages/storage/storage-domain/src/events.ts:46`](../../packages/storage/storage-domain/src/events.ts) + diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index 57b960da91..e6ead80f87 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/subagent.md -subagent.md: b26a12d1d50305d86d7ada29cac83474009d81ce -subagent.zh.md: 6c4c64ff22050b73699a97093acd0032668fde3d +# pnpm run verify-translation-pairing --write docs/subsystems/subagent.md +subagent.md: cbf9043b36a5cb2ab37bb99c6dbbf5a18d1e0288 +subagent.zh.md: ffd8fefd8b9d128c21e1b91ee0ba1fb706638fa9 diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index b26a12d1d5..cbf9043b36 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -433,3 +433,269 @@ The spawn and fork backends create an ordinary one-shot agent through `parent.ct - **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). + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.subagents` — `SubagentService` + +Named provider registry with one-shot runs, durable discovery, and continuable-child operations. + +```ts cordis-catalog +/** + * Establish one durable continuable child and deliver its initial prompt. + * Resolves when the child's inbox accepts that prompt, without waiting for the + * turn to start or for the message to reach the Session log; any earlier + * failure rejects with no ids and rolls back the child entirely. + * @param spec - provider, delegation request, and caller cancellation. + * @returns the durable child id and the accepted prompt's message id. + * @throws when continuation services are unavailable or materialization fails. + */ +async startContinuable(spec: ContinuableStartSpec): Promise + +/** + * Deliver one later message to a continuable child as its next FIFO turn. A + * resident child's Agent inbox accepts it directly (waking a `waiting` + * Activation), while an absent one is cold-resumed from its persisted + * Session. The Agent inbox is the only queue, so every accepted message has + * one observable order. + * @param parent - the exact live direct parent authorizing this delivery. + * @param childId - durable child session id. + * @param content - user-role content to deliver. + * @param options - durable provenance and caller cancellation, which stops the + * operation only before inbox acceptance. + * @returns the accepted message's inbox id. + * @throws when continuation services are unavailable, parent authority is + * rejected, or the message was not admitted. + */ +async followup( parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise + +/** + * Interrupt one live continuable child's current turn under a human parent + * address or an exact live ancestor Agent. Fire-and-return: the cancel + * signal is issued before this returns, but the target may keep running + * until it observes the signal. Unclaimed pending inbox work, the Activation, + * and published descendants are preserved; claimed work is not requeued. + * Once the interrupted driver is idle, a waking send resumes the parked FIFO + * queue. An absent target — including a one-shot or unknown id — + * is an accepted no-op, as is a manager-less composition, which cannot own a + * live Activation. + * @param targetSessionId - the durable child session id to interrupt. + * @param authority - the human parent address or exact live ancestor Agent. + * @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the + * live target. + */ +interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void + +/** + * Deliver selected content from one live continuable child to its durable + * direct parent. The child is the authority credential; callers cannot name a + * recipient. Reporting does not conclude the child's turn or Activation. + * @param child - exact live reporting child. + * @param content - selected model-facing content. + * @param options - parent scheduling and pre-acceptance cancellation. + * @returns the stable identity of the parent-accepted message. + * @throws when continuation services are unavailable, sender authorization + * fails, or the direct parent is not live. + */ +async reportFrom( child: Agent, content: ContentBlock[], options: SubagentReportOptions, ): Promise + +/** + * Compose one deployment capability into every continuable child's + * unpublished creation context on fresh creation and cold resume. Grants wait + * for the next Activation; removing the contribution revokes every resident + * installation immediately. + * @param contribution - synchronous child-scope installer. + * @returns the exact Cordis effect disposer. + */ +registerContinuableSetup(contribution: ContinuableSetupContribution): () => void + +/** + * Close continuable admission below exact live parent Agents, stop only their + * visible descendant Activations synchronously, then await admitted scoped + * materializations and release those forests child-first. The scoped cutoff + * lasts until each exact parent leaves the registry; unrelated parent trees + * remain live. + * @param parents - exact host-owned parent Agents entering teardown. + * @returns once every retained descendant Activation released its `AgentHandle`. + * @throws an aggregate error after all branches settle when any failed. + */ +async drainContinuableDescendants(parents: readonly Agent[]): Promise + +/** + * Enumerate the parent's direct session-backed subagents without loading or + * resuming an Agent and without any query seam: the listing merges the live + * session store with optional session persistence (live-preferred) and + * serves each child's durable mode/label from the registered `subagent` + * projection unit down a three-rung ladder — the registry's watermark + * snapshot for a live child; for a cold one, a durable projection-cache + * row when the optional cache serves an own-suffix identity (its `seq` + * gate proves the value postdates the fork seed, where a child's own + * descriptor is immutable once appended), else one persistence inspection + * folded through the registry. The + * projection fold is the single classification authority; per-child + * diagnostics relay a fold that served no identity or a failed inspection, + * never a list-time descriptor parse. Absent persistence, enumeration is + * live-only (a cold child cannot be resumed then either, so its absence is + * capability absence, not an error). This service consults no Agent + * registrations, Activations, or providers. + * + * Every persistence read receives `signal`, and the listing rechecks + * cancellation around each of those awaits. Read rejections that settle + * after an abort become a stable `SubagentError` with code `CANCELLED`. + * @param parentSessionId - parent session whose direct children are listed. + * @param signal - caller-owned cancellation forwarded to persistence reads + * and observed around every read await. + * @returns children and per-child diagnostics ordered by `createdAt`, then id. + * @throws {@link SubagentError} when the projection registry or the session + * store is not mounted, or the caller cancels the listing. + */ +listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise + +/** + * Enumerate the root's complete session-backed subagent tree in stable + * pre-order from one live-preferred corpus, without loading or resuming an + * Agent. Ordinary sessions and one-shot children remain traversal nodes so + * continuable descendants below them are discovered; each returned entry + * adds its durable `parentId` and root-relative `depth`. Identity resolution, + * diagnostics, optional persistence, and cancellation follow the same + * projection-backed contract as {@link listChildren}. + * @param rootSessionId - session whose complete descendant tree is listed. + * @param signal - caller-owned cancellation forwarded to persistence reads + * and observed around every read await. + * @returns children and per-candidate diagnostics with tree position, in + * stable pre-order. + * @throws {@link SubagentError} under the same conditions as {@link listChildren}. + */ +listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise + +/** + * Register a provider under its name. Registration is effect-scoped and HMR + * safe; removing a provider blocks new starts but does not revoke runs that + * were already returned to their holders. + * @param provider - the trusted provider implementation. + * @returns the exact Cordis effect disposer. + */ +registerProvider(provider: SubagentProvider): () => void + +/** + * Look up a provider by name. + * @param name - the provider name. + * @returns the provider, or undefined when absent. + */ +getProvider(name: string): SubagentProvider | undefined + +/** + * List registered provider names in insertion order. + * @returns the registered names. + */ +list(): string[] + +/** + * Establish a published child on the named provider. Capability and semantic + * checks run before delegation. Provider ownership lasts until its promise + * fulfills; a rejection therefore has no run for the caller to dispose and + * emits no run lifecycle events. Post-publication turn and infrastructure + * failures settle through the returned run. + * @param name - the provider to use. + * @param request - child label, prompt, parent, signal, and optional capabilities. + * @returns the published holder-owned run. + */ +async start(name: string, request: SubagentStartRequest): Promise +``` + +Types: [Agent](core.md) · [ContentBlock](core.md) · [MessageId](core.md) · [SessionId](core.md) + +Source: [`packages/subagent/subagent/src/index.ts:167`](../../packages/subagent/subagent/src/index.ts) + + + +### `subagent/*` events + + + +#### `subagent/end` — emit + +A published child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience. + +```ts cordis-catalog +/** + * A published child settled. Scope-filtered dispatch uses the same delegating + * parent carrier as `subagent/start`, so the lifecycle pair reaches the + * same scoped audience. + * @param info - the run identity and terminal outcome. + * @dshScopeScan unsupported + * @mode emit + */ +'subagent/end'(this: Scoped, info: SubagentRunEndInfo): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/subagent/subagent/src/index.ts:162`](../../packages/subagent/subagent/src/index.ts) + + + +#### `subagent/provider-added` — emit + +A provider became resolvable in the registry. + +```ts cordis-catalog +/** + * A provider became resolvable in the registry. + * @param provider - the registered provider. + * @mode emit + */ +'subagent/provider-added'(provider: SubagentProvider): void +``` + +Source: [`packages/subagent/subagent/src/index.ts:136`](../../packages/subagent/subagent/src/index.ts) + + + +#### `subagent/provider-removed` — emit + +A provider left the registry. Accepted runs remain holder-owned. + +```ts cordis-catalog +/** + * A provider left the registry. Accepted runs remain holder-owned. + * @param name - the provider name that no longer resolves. + * @mode emit + */ +'subagent/provider-removed'(name: string): void +``` + +Source: [`packages/subagent/subagent/src/index.ts:142`](../../packages/subagent/subagent/src/index.ts) + + + +#### `subagent/start` — emit + +A provider established a published child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`. + +```ts cordis-catalog +/** + * A provider established a published child. For in-process providers, + * `ctx.agents.get(info.id)` resolves during this notification. + * Scope-filtered dispatch keys the carrier by the delegating parent, so a + * parent-scoped listener observes only its own delegations. Paired with + * `subagent/end`. + * @param info - the provider and published child identity. + * @dshScopeScan unsupported + * @mode emit + */ +'subagent/start'(this: Scoped, info: SubagentRunInfo): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/subagent/src/index.ts) + diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index 6c4c64ff22..ffd8fefd8b 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -435,3 +435,269 @@ spawn 和 fork 后端通过 `parent.ctx` 创建一个普通的单次 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) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.subagents` — `SubagentService` + +Named provider registry with one-shot runs, durable discovery, and continuable-child operations. + +```ts cordis-catalog +/** + * Establish one durable continuable child and deliver its initial prompt. + * Resolves when the child's inbox accepts that prompt, without waiting for the + * turn to start or for the message to reach the Session log; any earlier + * failure rejects with no ids and rolls back the child entirely. + * @param spec - provider, delegation request, and caller cancellation. + * @returns the durable child id and the accepted prompt's message id. + * @throws when continuation services are unavailable or materialization fails. + */ +async startContinuable(spec: ContinuableStartSpec): Promise + +/** + * Deliver one later message to a continuable child as its next FIFO turn. A + * resident child's Agent inbox accepts it directly (waking a `waiting` + * Activation), while an absent one is cold-resumed from its persisted + * Session. The Agent inbox is the only queue, so every accepted message has + * one observable order. + * @param parent - the exact live direct parent authorizing this delivery. + * @param childId - durable child session id. + * @param content - user-role content to deliver. + * @param options - durable provenance and caller cancellation, which stops the + * operation only before inbox acceptance. + * @returns the accepted message's inbox id. + * @throws when continuation services are unavailable, parent authority is + * rejected, or the message was not admitted. + */ +async followup( parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise + +/** + * Interrupt one live continuable child's current turn under a human parent + * address or an exact live ancestor Agent. Fire-and-return: the cancel + * signal is issued before this returns, but the target may keep running + * until it observes the signal. Unclaimed pending inbox work, the Activation, + * and published descendants are preserved; claimed work is not requeued. + * Once the interrupted driver is idle, a waking send resumes the parked FIFO + * queue. An absent target — including a one-shot or unknown id — + * is an accepted no-op, as is a manager-less composition, which cannot own a + * live Activation. + * @param targetSessionId - the durable child session id to interrupt. + * @param authority - the human parent address or exact live ancestor Agent. + * @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the + * live target. + */ +interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void + +/** + * Deliver selected content from one live continuable child to its durable + * direct parent. The child is the authority credential; callers cannot name a + * recipient. Reporting does not conclude the child's turn or Activation. + * @param child - exact live reporting child. + * @param content - selected model-facing content. + * @param options - parent scheduling and pre-acceptance cancellation. + * @returns the stable identity of the parent-accepted message. + * @throws when continuation services are unavailable, sender authorization + * fails, or the direct parent is not live. + */ +async reportFrom( child: Agent, content: ContentBlock[], options: SubagentReportOptions, ): Promise + +/** + * Compose one deployment capability into every continuable child's + * unpublished creation context on fresh creation and cold resume. Grants wait + * for the next Activation; removing the contribution revokes every resident + * installation immediately. + * @param contribution - synchronous child-scope installer. + * @returns the exact Cordis effect disposer. + */ +registerContinuableSetup(contribution: ContinuableSetupContribution): () => void + +/** + * Close continuable admission below exact live parent Agents, stop only their + * visible descendant Activations synchronously, then await admitted scoped + * materializations and release those forests child-first. The scoped cutoff + * lasts until each exact parent leaves the registry; unrelated parent trees + * remain live. + * @param parents - exact host-owned parent Agents entering teardown. + * @returns once every retained descendant Activation released its `AgentHandle`. + * @throws an aggregate error after all branches settle when any failed. + */ +async drainContinuableDescendants(parents: readonly Agent[]): Promise + +/** + * Enumerate the parent's direct session-backed subagents without loading or + * resuming an Agent and without any query seam: the listing merges the live + * session store with optional session persistence (live-preferred) and + * serves each child's durable mode/label from the registered `subagent` + * projection unit down a three-rung ladder — the registry's watermark + * snapshot for a live child; for a cold one, a durable projection-cache + * row when the optional cache serves an own-suffix identity (its `seq` + * gate proves the value postdates the fork seed, where a child's own + * descriptor is immutable once appended), else one persistence inspection + * folded through the registry. The + * projection fold is the single classification authority; per-child + * diagnostics relay a fold that served no identity or a failed inspection, + * never a list-time descriptor parse. Absent persistence, enumeration is + * live-only (a cold child cannot be resumed then either, so its absence is + * capability absence, not an error). This service consults no Agent + * registrations, Activations, or providers. + * + * Every persistence read receives `signal`, and the listing rechecks + * cancellation around each of those awaits. Read rejections that settle + * after an abort become a stable `SubagentError` with code `CANCELLED`. + * @param parentSessionId - parent session whose direct children are listed. + * @param signal - caller-owned cancellation forwarded to persistence reads + * and observed around every read await. + * @returns children and per-child diagnostics ordered by `createdAt`, then id. + * @throws {@link SubagentError} when the projection registry or the session + * store is not mounted, or the caller cancels the listing. + */ +listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise + +/** + * Enumerate the root's complete session-backed subagent tree in stable + * pre-order from one live-preferred corpus, without loading or resuming an + * Agent. Ordinary sessions and one-shot children remain traversal nodes so + * continuable descendants below them are discovered; each returned entry + * adds its durable `parentId` and root-relative `depth`. Identity resolution, + * diagnostics, optional persistence, and cancellation follow the same + * projection-backed contract as {@link listChildren}. + * @param rootSessionId - session whose complete descendant tree is listed. + * @param signal - caller-owned cancellation forwarded to persistence reads + * and observed around every read await. + * @returns children and per-candidate diagnostics with tree position, in + * stable pre-order. + * @throws {@link SubagentError} under the same conditions as {@link listChildren}. + */ +listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise + +/** + * Register a provider under its name. Registration is effect-scoped and HMR + * safe; removing a provider blocks new starts but does not revoke runs that + * were already returned to their holders. + * @param provider - the trusted provider implementation. + * @returns the exact Cordis effect disposer. + */ +registerProvider(provider: SubagentProvider): () => void + +/** + * Look up a provider by name. + * @param name - the provider name. + * @returns the provider, or undefined when absent. + */ +getProvider(name: string): SubagentProvider | undefined + +/** + * List registered provider names in insertion order. + * @returns the registered names. + */ +list(): string[] + +/** + * Establish a published child on the named provider. Capability and semantic + * checks run before delegation. Provider ownership lasts until its promise + * fulfills; a rejection therefore has no run for the caller to dispose and + * emits no run lifecycle events. Post-publication turn and infrastructure + * failures settle through the returned run. + * @param name - the provider to use. + * @param request - child label, prompt, parent, signal, and optional capabilities. + * @returns the published holder-owned run. + */ +async start(name: string, request: SubagentStartRequest): Promise +``` + +Types: [Agent](core.md) · [ContentBlock](core.md) · [MessageId](core.md) · [SessionId](core.md) + +Source: [`packages/subagent/subagent/src/index.ts:167`](../../packages/subagent/subagent/src/index.ts) + + + +### `subagent/*` events + + + +#### `subagent/end` — emit + +A published child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience. + +```ts cordis-catalog +/** + * A published child settled. Scope-filtered dispatch uses the same delegating + * parent carrier as `subagent/start`, so the lifecycle pair reaches the + * same scoped audience. + * @param info - the run identity and terminal outcome. + * @dshScopeScan unsupported + * @mode emit + */ +'subagent/end'(this: Scoped, info: SubagentRunEndInfo): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/subagent/subagent/src/index.ts:162`](../../packages/subagent/subagent/src/index.ts) + + + +#### `subagent/provider-added` — emit + +A provider became resolvable in the registry. + +```ts cordis-catalog +/** + * A provider became resolvable in the registry. + * @param provider - the registered provider. + * @mode emit + */ +'subagent/provider-added'(provider: SubagentProvider): void +``` + +Source: [`packages/subagent/subagent/src/index.ts:136`](../../packages/subagent/subagent/src/index.ts) + + + +#### `subagent/provider-removed` — emit + +A provider left the registry. Accepted runs remain holder-owned. + +```ts cordis-catalog +/** + * A provider left the registry. Accepted runs remain holder-owned. + * @param name - the provider name that no longer resolves. + * @mode emit + */ +'subagent/provider-removed'(name: string): void +``` + +Source: [`packages/subagent/subagent/src/index.ts:142`](../../packages/subagent/subagent/src/index.ts) + + + +#### `subagent/start` — emit + +A provider established a published child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`. + +```ts cordis-catalog +/** + * A provider established a published child. For in-process providers, + * `ctx.agents.get(info.id)` resolves during this notification. + * Scope-filtered dispatch keys the carrier by the delegating parent, so a + * parent-scoped listener observes only its own delegations. Paired with + * `subagent/end`. + * @param info - the provider and published child identity. + * @dshScopeScan unsupported + * @mode emit + */ +'subagent/start'(this: Scoped, info: SubagentRunInfo): void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/subagent/src/index.ts) + diff --git a/docs/subsystems/subprocess.i18n.yaml b/docs/subsystems/subprocess.i18n.yaml index b2d38854b2..a3071e98a9 100644 --- a/docs/subsystems/subprocess.i18n.yaml +++ b/docs/subsystems/subprocess.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/subprocess.md -subprocess.md: 023b122218ad1caa2b8e16c26b0bc8b0d4183c28 -subprocess.zh.md: 5ee9c782c0ce73ca9a2e694450ed38b8d548dc09 +# pnpm run verify-translation-pairing --write docs/subsystems/subprocess.md +subprocess.md: 0bc2bf86518bcc60bef02554b94471425f2ac433 +subprocess.zh.md: 5d6425707edcf1952c25dbdc91dbf7aab3779350 diff --git a/docs/subsystems/subprocess.md b/docs/subsystems/subprocess.md index 023b122218..0bc2bf8651 100644 --- a/docs/subsystems/subprocess.md +++ b/docs/subsystems/subprocess.md @@ -242,8 +242,83 @@ interface SubprocessOutcome { `spawnTerminal(spec)` is the non-pipe process primitive. The provider allocates the controlling terminal and owns UTF-8 text transport, foreground-process-group inspection and signalling, and one awaited TERM-to-KILL operation that reaches quiescence for every session member the provider can still observe; providers document substrate-specific observability limits. The PTY backend remains responsible for prompt detection, readiness inference, scrollback, sandbox policy, and persistent-session ownership; ordinary `spawn()` cannot reconstruct controlling-terminal semantics. -The terminal spec fully specifies argv, cwd, environment overrides, dimensions, cleanup grace, and optional allocation cancellation. Its handle exposes `pid`, ordered output, `done`, `write`, `inspectForeground`, `signalForeground`, and awaited `terminate`; the exact public shapes are generated into the [`ctx.subprocess` service catalog](../cordis-catalog/services.md#ctxsubprocess--subprocessservice-abstract-seam). +The terminal spec fully specifies argv, cwd, environment overrides, dimensions, cleanup grace, and optional allocation cancellation. Its handle exposes `pid`, ordered output, `done`, `write`, `inspectForeground`, `signalForeground`, and awaited `terminate`; the exact public shapes are generated into the [`ctx.subprocess` service catalog](#ctxsubprocess--subprocessservice-abstract-seam). ## Service behavior The abstract [`SubprocessService`](../../packages/subprocess/subprocess/src/index.ts) seam defines execution-world coordinates, executable lookup, ordinary `spawn`, and `spawnTerminal`. [`LocalSubprocessService`](../../packages/subprocess/subprocess-local/src/index.ts) implements them with detached process trees, per-disposition wiring, credential scrubbing, `node-pty`, platform process inspection, and terminate-and-join disposal. See [`dsh-subprocess`](../../packages/subprocess/subprocess/README.md) for the interface contract and [`dsh-subprocess-local`](../../packages/subprocess/subprocess-local/README.md) for local mechanics. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.e2b` — `E2BSandboxService` + +Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal. Creation begins at plugin construction; adapters await getSandbox before their first operation. + +```ts cordis-catalog +/** + * Return the shared live SDK handle. + * @returns the created sandbox after the configured cwd exists. + * @throws when E2B rejects creation or the service is disposing. + */ +async getSandbox(): Promise +``` + +Source: [`packages/e2b/e2b/src/index.ts:74`](../../packages/e2b/e2b/src/index.ts) + + + +### `ctx.subprocess` — `SubprocessService` (abstract seam) + +Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). + +Implementations must honor these semantics: + +- Executable paths belong to one execution world shared with the mounted filesystem provider. +- spawn returns immediately with a live handle; `done` resolves at process close with exit facts and rejects only for spawn-level failures. +- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here. +- SubprocessHandle.terminate (and the spec's abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — tree-scoped on every platform. SubprocessHandle.waitForExit observes whole-tree liveness, so a consumer-owned teardown ladder can hold each tier on real quiescence. +- Disposal of the service terminates all still-running managed processes and awaits their exit. +- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits. + +```ts cordis-catalog +/** + * Resolve one configured executable in this provider's execution world. + * Absolute paths are verified; bare names use the provider's scrubbed PATH + * plus explicit environment overrides. Relative paths containing separators + * are rejected: no current consumer defines which directory they would + * resolve against, so providers fail loud instead of guessing. + * @param command - absolute executable path or bare PATH name. + * @param env - explicit environment entries used for lookup. + * @param signal - aborts remote or local lookup. + * @returns a canonical executable path. + */ +abstract resolveExecutable( command: string, env?: Readonly>, signal?: AbortSignal, ): Promise + +/** + * Start one managed child process from a fully-specified spec; this seam + * applies no defaults. + * @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment. + * @returns the live process handle (streams/readers, signalling, outcome promise). + */ +abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle + +/** + * Allocate a real terminal and start one owned process session. This is the + * only non-pipe process primitive: implementations own terminal byte I/O, + * foreground groups, signals, and complete session-tree cleanup. + * @param spec - fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation. + * @returns the live terminal handle after allocation succeeds. + */ +abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise +``` + +Source: [`packages/subprocess/subprocess/src/index.ts:102`](../../packages/subprocess/subprocess/src/index.ts) + diff --git a/docs/subsystems/subprocess.zh.md b/docs/subsystems/subprocess.zh.md index 5ee9c782c0..5d6425707e 100644 --- a/docs/subsystems/subprocess.zh.md +++ b/docs/subsystems/subprocess.zh.md @@ -242,8 +242,83 @@ interface SubprocessOutcome { `spawnTerminal(spec)` 是非管道进程原语。提供方分配控制终端,并负责 UTF-8 文本传输、前台进程组检查与信号发送,以及一项须等待的 TERM→KILL 操作;该操作会使提供方仍可观察到的每个会话成员完全停稳,提供方则会记录执行基底特有的可观察性限制。PTY 后端仍负责提示符检测、就绪推断、scrollback、沙箱策略和持久会话所有权;普通 `spawn()` 无法重建控制终端语义。 -终端 spec 完全指定 argv、cwd、环境覆盖、尺寸、清理宽限期与可选的分配取消。其句柄公开 `pid`、有序输出、`done`、`write`、`inspectForeground`、`signalForeground` 和须等待的 `terminate`;确切的公共形状生成到 [`ctx.subprocess` 服务目录](../cordis-catalog/services.md#ctxsubprocess--subprocessservice-abstract-seam)中。 +终端 spec 完全指定 argv、cwd、环境覆盖、尺寸、清理宽限期与可选的分配取消。其句柄公开 `pid`、有序输出、`done`、`write`、`inspectForeground`、`signalForeground` 和须等待的 `terminate`;确切的公共形状生成到 [`ctx.subprocess` 服务目录](#ctxsubprocess--subprocessservice-abstract-seam)中。 ## 服务行为 抽象的 [`SubprocessService`](../../packages/subprocess/subprocess/src/index.ts) seam 定义执行世界坐标、可执行文件查找、普通 `spawn` 与 `spawnTerminal`。[`LocalSubprocessService`](../../packages/subprocess/subprocess-local/src/index.ts) 以 detached 进程树、按处置方式接线、凭据清除、`node-pty`、平台进程检查,以及先终止再等待退出的资源释放实现这些能力。接口契约见 [`dsh-subprocess`](../../packages/subprocess/subprocess/README.md),本地机制见 [`dsh-subprocess-local`](../../packages/subprocess/subprocess-local/README.md)。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.e2b` — `E2BSandboxService` + +Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal. Creation begins at plugin construction; adapters await getSandbox before their first operation. + +```ts cordis-catalog +/** + * Return the shared live SDK handle. + * @returns the created sandbox after the configured cwd exists. + * @throws when E2B rejects creation or the service is disposing. + */ +async getSandbox(): Promise +``` + +Source: [`packages/e2b/e2b/src/index.ts:74`](../../packages/e2b/e2b/src/index.ts) + + + +### `ctx.subprocess` — `SubprocessService` (abstract seam) + +Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). + +Implementations must honor these semantics: + +- Executable paths belong to one execution world shared with the mounted filesystem provider. +- spawn returns immediately with a live handle; `done` resolves at process close with exit facts and rejects only for spawn-level failures. +- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here. +- SubprocessHandle.terminate (and the spec's abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — tree-scoped on every platform. SubprocessHandle.waitForExit observes whole-tree liveness, so a consumer-owned teardown ladder can hold each tier on real quiescence. +- Disposal of the service terminates all still-running managed processes and awaits their exit. +- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits. + +```ts cordis-catalog +/** + * Resolve one configured executable in this provider's execution world. + * Absolute paths are verified; bare names use the provider's scrubbed PATH + * plus explicit environment overrides. Relative paths containing separators + * are rejected: no current consumer defines which directory they would + * resolve against, so providers fail loud instead of guessing. + * @param command - absolute executable path or bare PATH name. + * @param env - explicit environment entries used for lookup. + * @param signal - aborts remote or local lookup. + * @returns a canonical executable path. + */ +abstract resolveExecutable( command: string, env?: Readonly>, signal?: AbortSignal, ): Promise + +/** + * Start one managed child process from a fully-specified spec; this seam + * applies no defaults. + * @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment. + * @returns the live process handle (streams/readers, signalling, outcome promise). + */ +abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle + +/** + * Allocate a real terminal and start one owned process session. This is the + * only non-pipe process primitive: implementations own terminal byte I/O, + * foreground groups, signals, and complete session-tree cleanup. + * @param spec - fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation. + * @returns the live terminal handle after allocation succeeds. + */ +abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise +``` + +Source: [`packages/subprocess/subprocess/src/index.ts:102`](../../packages/subprocess/subprocess/src/index.ts) + diff --git a/docs/subsystems/system-prompt.i18n.yaml b/docs/subsystems/system-prompt.i18n.yaml index 5c39e3cbcb..a1e14385f4 100644 --- a/docs/subsystems/system-prompt.i18n.yaml +++ b/docs/subsystems/system-prompt.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/system-prompt.md -system-prompt.md: 59193c1881abcadbc8a1778cde92f5a6572eee24 -system-prompt.zh.md: 41e45417817895ccf6def70e510eca7422a65e41 +system-prompt.md: 94ce40f8bf98dd4efe3514879c2527c2a7bd3b21 +system-prompt.zh.md: 37d366774089f91756433602a719e99b57747c17 diff --git a/docs/subsystems/system-prompt.md b/docs/subsystems/system-prompt.md index 59193c1881..94ce40f8bf 100644 --- a/docs/subsystems/system-prompt.md +++ b/docs/subsystems/system-prompt.md @@ -76,3 +76,113 @@ interface PromptContext { readonly text: string | ((context: AssembleContext) => string) } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.systemPrompt` — `SystemPrompt` + +Registry service for the prompt inputs assembled before each model step. + +```ts cordis-catalog +/** + * Register an ordered prompt section in the calling context's scope. A scoped + * section shadows a global section with the same name; duplicates within one + * layer and non-finite orders throw. Registration and disposal emit + * `system-prompt/change`. + * @param section - the section to register. + * @returns the exact Cordis effect disposer. + */ +section(section: PromptSection): () => void + +/** + * Register ordered dynamic context in the calling context's scope. Scoped + * entries shadow global entries with the same name. + * @param context - the context contribution to register. + * @returns the exact Cordis effect disposer. + */ +context(context: PromptContext): () => void + +/** + * Register a tool-schema provider in the calling context's scope. Global and + * matching scoped providers both contribute; returning the reserved + * {@link TOOL_ORDER_REST} name makes assembly fail. + * @param provider - evaluated for each assembly with its context. + * @returns the exact Cordis effect disposer. + */ +tools(provider: (context: AssembleContext) => ToolProviderResult): () => void + +/** + * Register a prompt variable in the calling context's scope. Scoped values + * shadow globals; invalid or duplicate names throw. A provider may return + * `undefined`, but rendering a section that references that value then fails. + * @param name - the `[a-z][a-z0-9_]*` reference name. + * @param provider - evaluated for each assembly. + * @returns the exact Cordis effect disposer. + */ +variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void + +/** + * Assemble global and scoped providers, detach tool parameters, apply + * canonical ordering, then run the assembly waterfall. Scoped sections and + * variables shadow globals; the returned waterfall value is authoritative. + * @param context - the optional scope and plugin-defined assembly fields. + * @returns the authoritative post-waterfall assembly. + */ +async assemble(context: AssembleContext = {}): Promise +``` + +Source: [`packages/core/system-prompt/src/index.ts:314`](../../packages/core/system-prompt/src/index.ts) + + + +### `system-prompt/*` events + + + +#### `system-prompt/assemble` — waterfall + +Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. + +```ts cordis-catalog +/** + * Expert waterfall over the assembled sections, contexts, tools, and variables. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners + * receive only that scope's assemblies. The returned value is authoritative. + * A supplied signal controls only this explicit assembly request and must not + * be retained to control later turns. + * @param assembly - the mutable assembly built from registered providers. + * @param context - the caller's per-assembly context. + * @mode waterfall + */ +'system-prompt/assemble'(this: Scoped, assembly: PromptAssembly, context: AssembleContext, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/system-prompt/src/index.ts:29`](../../packages/core/system-prompt/src/index.ts) + + + +#### `system-prompt/change` — emit + +Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope. + +```ts cordis-catalog +/** + * Emitted when any prompt provider changes. This registry notification is + * unfiltered because a global change affects every scope. + * @mode emit + */ +'system-prompt/change'(): void +``` + +Source: [`packages/core/system-prompt/src/index.ts:35`](../../packages/core/system-prompt/src/index.ts) + diff --git a/docs/subsystems/system-prompt.zh.md b/docs/subsystems/system-prompt.zh.md index 41e4541781..37d3667740 100644 --- a/docs/subsystems/system-prompt.zh.md +++ b/docs/subsystems/system-prompt.zh.md @@ -76,3 +76,113 @@ interface PromptContext { readonly text: string | ((context: AssembleContext) => string) } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.systemPrompt` — `SystemPrompt` + +Registry service for the prompt inputs assembled before each model step. + +```ts cordis-catalog +/** + * Register an ordered prompt section in the calling context's scope. A scoped + * section shadows a global section with the same name; duplicates within one + * layer and non-finite orders throw. Registration and disposal emit + * `system-prompt/change`. + * @param section - the section to register. + * @returns the exact Cordis effect disposer. + */ +section(section: PromptSection): () => void + +/** + * Register ordered dynamic context in the calling context's scope. Scoped + * entries shadow global entries with the same name. + * @param context - the context contribution to register. + * @returns the exact Cordis effect disposer. + */ +context(context: PromptContext): () => void + +/** + * Register a tool-schema provider in the calling context's scope. Global and + * matching scoped providers both contribute; returning the reserved + * {@link TOOL_ORDER_REST} name makes assembly fail. + * @param provider - evaluated for each assembly with its context. + * @returns the exact Cordis effect disposer. + */ +tools(provider: (context: AssembleContext) => ToolProviderResult): () => void + +/** + * Register a prompt variable in the calling context's scope. Scoped values + * shadow globals; invalid or duplicate names throw. A provider may return + * `undefined`, but rendering a section that references that value then fails. + * @param name - the `[a-z][a-z0-9_]*` reference name. + * @param provider - evaluated for each assembly. + * @returns the exact Cordis effect disposer. + */ +variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void + +/** + * Assemble global and scoped providers, detach tool parameters, apply + * canonical ordering, then run the assembly waterfall. Scoped sections and + * variables shadow globals; the returned waterfall value is authoritative. + * @param context - the optional scope and plugin-defined assembly fields. + * @returns the authoritative post-waterfall assembly. + */ +async assemble(context: AssembleContext = {}): Promise +``` + +Source: [`packages/core/system-prompt/src/index.ts:314`](../../packages/core/system-prompt/src/index.ts) + + + +### `system-prompt/*` events + + + +#### `system-prompt/assemble` — waterfall + +Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. + +```ts cordis-catalog +/** + * Expert waterfall over the assembled sections, contexts, tools, and variables. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners + * receive only that scope's assemblies. The returned value is authoritative. + * A supplied signal controls only this explicit assembly request and must not + * be retained to control later turns. + * @param assembly - the mutable assembly built from registered providers. + * @param context - the caller's per-assembly context. + * @mode waterfall + */ +'system-prompt/assemble'(this: Scoped, assembly: PromptAssembly, context: AssembleContext, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/system-prompt/src/index.ts:29`](../../packages/core/system-prompt/src/index.ts) + + + +#### `system-prompt/change` — emit + +Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope. + +```ts cordis-catalog +/** + * Emitted when any prompt provider changes. This registry notification is + * unfiltered because a global change affects every scope. + * @mode emit + */ +'system-prompt/change'(): void +``` + +Source: [`packages/core/system-prompt/src/index.ts:35`](../../packages/core/system-prompt/src/index.ts) + diff --git a/docs/subsystems/tasks.i18n.yaml b/docs/subsystems/tasks.i18n.yaml index 58920a53df..d28dcc2781 100644 --- a/docs/subsystems/tasks.i18n.yaml +++ b/docs/subsystems/tasks.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/tasks.md -tasks.md: a38055d3ef7aa18e62678f92eb5ac5ae2a09c205 -tasks.zh.md: f34d42e713c3a0c11cbf88d52e573bb100c52493 +# pnpm run verify-translation-pairing --write docs/subsystems/tasks.md +tasks.md: 6237607307cbab8511f968ed591e3e3b8b845c6a +tasks.zh.md: 99a6fcc4f54c1b52e5c6fd393327cc768f064297 diff --git a/docs/subsystems/tasks.md b/docs/subsystems/tasks.md index a38055d3ef..6237607307 100644 --- a/docs/subsystems/tasks.md +++ b/docs/subsystems/tasks.md @@ -152,3 +152,109 @@ interface TaskRead { ## Service behavior The abstract [`TaskService`](../../packages/tasks/tasks/src/index.ts) seam defines atomic `start`, caller-scoped `get` and `list`, `read`, `kill`, bounded `wait`, contained `onTaskDone` listeners, and the `attachSurface` availability fence; [`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) is the process-local implementation. Authorization compares owner sessions; owner cleanup selects the exact registered `Agent` instance. See [`dsh-tasks`](../../packages/tasks/tasks/README.md) for the seam contract, [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md) for the registry lifecycle, and [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md) for the model-facing surface. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.tasks` — `TaskService` (abstract seam) + +Abstract background task registry. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.tasks` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). + +Implementations must honor these semantics: + +- Registrations outlive producer and control-surface fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. +- Owned-task access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary. +- Settlement is first-wins: one terminal record, one round of contained listener notification, and released waiters, even against a late producer outcome. +- start refuses work while no control surface is attached, so a producer cannot start work that callers cannot collect or stop. + +```ts cordis-catalog +/** + * Preflight access, validation, and owner cleanup before starting and + * atomically registering work. A throwing starter leaves nothing registered; + * after it returns, registration cannot fail. Settlement records the outcome, + * notifies listeners, and releases waiters. + * @param spec - task identity, owner, and synchronous starter. + * @returns the registry-issued `-N` id. + */ +abstract start(spec: TaskStart): TaskId + +/** + * List caller-owned and unowned tasks in registration order without exposing + * another session's labels. + * @param caller - reading agent; a non-agent caller sees only unowned tasks. + * @returns fresh snapshots. + */ +abstract list(caller?: Agent): TaskSnapshot[] + +/** + * Return a non-consuming snapshot without changing its read cursor or notice + * state. Throws for an unknown or foreign task. + * @param id - task to look up. + * @param caller - reading agent checked against the owner. + * @returns a fresh snapshot. + */ +abstract get(id: TaskId, caller?: Agent): TaskSnapshot + +/** + * Read the next stream delta, or the idempotent final output after settlement. + * A terminal read marks the task reported. Throws for an unknown or foreign + * task. + * @param id - task to read. + * @param caller - reading agent checked against the owner. + * @returns output text and the post-read snapshot. + */ +abstract read(id: TaskId, caller?: Agent): TaskRead + +/** + * Request cancellation, then mark the task stopping and reported. A producer + * throw propagates without changing task state. Throws for an unknown or + * foreign task. + * @param id - task to cancel. + * @param caller - killing agent checked against the owner. + * @param reason - logged reason forwarded to the producer. + * @returns `requested` for live work, otherwise `already-finished`. + */ +abstract kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished' + +/** + * Wait for settlement or timeout without cancelling the task. Caller abort + * rejects only while the task is live; after settlement the terminal + * snapshot wins so a notice suppressed for this waiter is still delivered. + * Throws for invalid, unknown, or foreign input. + * @param id - task to wait for. + * @param timeoutMs - positive finite wait bound in milliseconds. + * @param caller - waiting agent checked against the owner. + * @param signal - optional cancellation of the wait itself. + * @returns snapshot at settlement or timeout. + */ +abstract wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise + +/** + * Register an effect-scoped completion listener. Each listener is contained; + * returned promises are observed but not awaited. No listener runs after + * service disposal. + * @param listener - receives each terminal snapshot and its exact owner. + * @returns disposer that unregisters the listener. + */ +abstract onTaskDone(listener: TaskDoneListener): () => void + +/** + * Attach an effect-scoped surface that can read and stop tasks. {@link start} + * refuses work while none is attached. + * @param name - diagnostic label; duplicate names remain independent. + * @returns disposer that detaches this surface. + */ +abstract attachSurface(name: string): () => void +``` + +Types: [Agent](core.md) + +Source: [`packages/tasks/tasks/src/index.ts:50`](../../packages/tasks/tasks/src/index.ts) + diff --git a/docs/subsystems/tasks.zh.md b/docs/subsystems/tasks.zh.md index f34d42e713..99a6fcc4f5 100644 --- a/docs/subsystems/tasks.zh.md +++ b/docs/subsystems/tasks.zh.md @@ -152,3 +152,109 @@ interface TaskRead { ## 服务行为 抽象的 [`TaskService`](../../packages/tasks/tasks/src/index.ts) seam 定义原子 `start`、限定调用方作用域的 `get` 和 `list`、`read`、`kill`、有界 `wait`、故障隔离的 `onTaskDone` 监听器,以及 `attachSurface` 可用性防线;[`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) 是其进程局部实现。授权会比较拥有者会话;拥有者清理会选择确切的已注册 `Agent` 实例。seam 契约见 [`dsh-tasks`](../../packages/tasks/tasks/README.md),注册表生命周期见 [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md),面向模型的接口见 [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md)。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.tasks` — `TaskService` (abstract seam) + +Abstract background task registry. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.tasks` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). + +Implementations must honor these semantics: + +- Registrations outlive producer and control-surface fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. +- Owned-task access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary. +- Settlement is first-wins: one terminal record, one round of contained listener notification, and released waiters, even against a late producer outcome. +- start refuses work while no control surface is attached, so a producer cannot start work that callers cannot collect or stop. + +```ts cordis-catalog +/** + * Preflight access, validation, and owner cleanup before starting and + * atomically registering work. A throwing starter leaves nothing registered; + * after it returns, registration cannot fail. Settlement records the outcome, + * notifies listeners, and releases waiters. + * @param spec - task identity, owner, and synchronous starter. + * @returns the registry-issued `-N` id. + */ +abstract start(spec: TaskStart): TaskId + +/** + * List caller-owned and unowned tasks in registration order without exposing + * another session's labels. + * @param caller - reading agent; a non-agent caller sees only unowned tasks. + * @returns fresh snapshots. + */ +abstract list(caller?: Agent): TaskSnapshot[] + +/** + * Return a non-consuming snapshot without changing its read cursor or notice + * state. Throws for an unknown or foreign task. + * @param id - task to look up. + * @param caller - reading agent checked against the owner. + * @returns a fresh snapshot. + */ +abstract get(id: TaskId, caller?: Agent): TaskSnapshot + +/** + * Read the next stream delta, or the idempotent final output after settlement. + * A terminal read marks the task reported. Throws for an unknown or foreign + * task. + * @param id - task to read. + * @param caller - reading agent checked against the owner. + * @returns output text and the post-read snapshot. + */ +abstract read(id: TaskId, caller?: Agent): TaskRead + +/** + * Request cancellation, then mark the task stopping and reported. A producer + * throw propagates without changing task state. Throws for an unknown or + * foreign task. + * @param id - task to cancel. + * @param caller - killing agent checked against the owner. + * @param reason - logged reason forwarded to the producer. + * @returns `requested` for live work, otherwise `already-finished`. + */ +abstract kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished' + +/** + * Wait for settlement or timeout without cancelling the task. Caller abort + * rejects only while the task is live; after settlement the terminal + * snapshot wins so a notice suppressed for this waiter is still delivered. + * Throws for invalid, unknown, or foreign input. + * @param id - task to wait for. + * @param timeoutMs - positive finite wait bound in milliseconds. + * @param caller - waiting agent checked against the owner. + * @param signal - optional cancellation of the wait itself. + * @returns snapshot at settlement or timeout. + */ +abstract wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise + +/** + * Register an effect-scoped completion listener. Each listener is contained; + * returned promises are observed but not awaited. No listener runs after + * service disposal. + * @param listener - receives each terminal snapshot and its exact owner. + * @returns disposer that unregisters the listener. + */ +abstract onTaskDone(listener: TaskDoneListener): () => void + +/** + * Attach an effect-scoped surface that can read and stop tasks. {@link start} + * refuses work while none is attached. + * @param name - diagnostic label; duplicate names remain independent. + * @returns disposer that detaches this surface. + */ +abstract attachSurface(name: string): () => void +``` + +Types: [Agent](core.md) + +Source: [`packages/tasks/tasks/src/index.ts:50`](../../packages/tasks/tasks/src/index.ts) + diff --git a/docs/subsystems/telemetry.i18n.yaml b/docs/subsystems/telemetry.i18n.yaml index 2eb634bb2d..19cd6f6851 100644 --- a/docs/subsystems/telemetry.i18n.yaml +++ b/docs/subsystems/telemetry.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/telemetry.md -telemetry.md: cf570723650c32f048ee797923a58f09c777455d -telemetry.zh.md: 1bcaa86264e3afd741d66d6ee718946e6151496f +telemetry.md: 131c5814f29d70c356989e62fda1db5c1c5448d6 +telemetry.zh.md: 5d4a9d9994457bc06a9e4194c4807f88789a3cf0 diff --git a/docs/subsystems/telemetry.md b/docs/subsystems/telemetry.md index cf57072365..131c5814f2 100644 --- a/docs/subsystems/telemetry.md +++ b/docs/subsystems/telemetry.md @@ -105,8 +105,76 @@ interface TelemetryBackend { } ``` -`Telemetry` (`ctx.telemetry`, [signatures](../cordis-catalog/services.md#ctxtelemetry--telemetry-abstract-seam)) is the contract's loadable form — one implementation per context, duplicate load throws — and a backend composes the seam's `TelemetryCoordinator` in its constructor to install the capture side. +`Telemetry` (`ctx.telemetry`, [signatures](#ctxtelemetry--telemetry-abstract-seam)) is the contract's loadable form — one implementation per context, duplicate load throws — and a backend composes the seam's `TelemetryCoordinator` in its constructor to install the capture side. ## The redact waterfall: `telemetry/record` -Every record passes the `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics) between projection and `emit()` ([event entry](../cordis-catalog/events.md#telemetryrecord--waterfall)). The seam ships NO rules of its own: with no listener mounted, records reach the backend exactly as captured, so exported data is precisely as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath; a throwing listener withholds that one record fail-closed inside the coordinator's containment. Redaction applies to the exported copy only — the canonical session log is never rewritten. +Every record passes the `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics) between projection and `emit()` ([event entry](#telemetryrecord--waterfall)). The seam ships NO rules of its own: with no listener mounted, records reach the backend exactly as captured, so exported data is precisely as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath; a throwing listener withholds that one record fail-closed inside the coordinator's containment. Redaction applies to the exported copy only — the canonical session log is never rewritten. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.telemetry` — `Telemetry` (abstract seam) + +The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. + +```ts cordis-catalog +/** + * See {@link TelemetryBackend.emit} — the seam declaration is the contract's one home. + * @param record - the logical record to report; owned by the backend after the call. + */ +abstract emit(record: TelemetryRecord): void + +/** See {@link TelemetryBackend.flush}. */ +flush?(): void + +/** + * See {@link TelemetryBackend.shutdown}. + * @returns resolves when the backend's pipeline has quiesced. + */ +abstract shutdown(): Promise +``` + +Source: [`packages/session/session-telemetry/src/index.ts:140`](../../packages/session/session-telemetry/src/index.ts) + + + +### `telemetry/*` events + + + +#### `telemetry/record` — waterfall + +Transform one outbound record before it reaches the backend. This waterfall is the seam's redaction extension point. It ships NO rules of its own: the innermost `next()` passes the record through unchanged, and with no listener mounted records reach the backend as captured, so exported data is exactly as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath. Dispatched synchronously on the capture hot path inside the coordinator's containment: a throwing listener withholds that one record (fail-closed) and never reaches the agent loop. Live capture dispatches at append time; on-demand capture dispatches while reading the canonical log. Redaction applies to the exported copy only; the canonical session log is never rewritten. + +```ts cordis-catalog +/** + * Transform one outbound record before it reaches the backend. This + * waterfall is the seam's redaction extension point. It ships NO rules + * of its own: the + * innermost `next()` passes the record through unchanged, and with no + * listener mounted records reach the backend as captured, so exported + * data is exactly as clean as the rules a deployment mounts. Listeners + * stack by transforming `next()`'s return value; returning without + * `next()` replaces everything beneath. Dispatched synchronously on the + * capture hot path inside the coordinator's containment: a throwing + * listener withholds that one record (fail-closed) and never reaches the + * agent loop. Live capture dispatches at append time; on-demand capture + * dispatches while reading the canonical log. Redaction applies to the + * exported copy only; the canonical session log is never rewritten. + * @param record - the candidate record, already the coordinator's own deep + * copy; listeners return a (possibly new) record and must not mutate it. + * @mode waterfall + */ +'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord +``` + +Source: [`packages/session/session-telemetry/src/index.ts:43`](../../packages/session/session-telemetry/src/index.ts) + diff --git a/docs/subsystems/telemetry.zh.md b/docs/subsystems/telemetry.zh.md index 1bcaa86264..5d4a9d9994 100644 --- a/docs/subsystems/telemetry.zh.md +++ b/docs/subsystems/telemetry.zh.md @@ -105,8 +105,76 @@ interface TelemetryBackend { } ``` -`Telemetry`(`ctx.telemetry`,[签名](../cordis-catalog/services.md#ctxtelemetry--telemetry-abstract-seam))是该契约的可加载形态:每个上下文只允许一个实现,重复加载会抛出异常;后端在其构造函数中组合 seam 的 `TelemetryCoordinator`,以此装配捕获侧。 +`Telemetry`(`ctx.telemetry`,[签名](#ctxtelemetry--telemetry-abstract-seam))是该契约的可加载形态:每个上下文只允许一个实现,重复加载会抛出异常;后端在其构造函数中组合 seam 的 `TelemetryCoordinator`,以此装配捕获侧。 ## 脱敏 waterfall:`telemetry/record` -每条记录在投影与 `emit()` 之间都要经过 `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics)([事件条目](../cordis-catalog/events.md#telemetryrecord--waterfall))。seam 自身不带任何规则:未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式扣下这一条记录。脱敏只作用于导出副本;权威会话日志永不改写。 +每条记录在投影与 `emit()` 之间都要经过 `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics)([事件条目](#telemetryrecord--waterfall))。seam 自身不带任何规则:未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式扣下这一条记录。脱敏只作用于导出副本;权威会话日志永不改写。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.telemetry` — `Telemetry` (abstract seam) + +The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side. + +```ts cordis-catalog +/** + * See {@link TelemetryBackend.emit} — the seam declaration is the contract's one home. + * @param record - the logical record to report; owned by the backend after the call. + */ +abstract emit(record: TelemetryRecord): void + +/** See {@link TelemetryBackend.flush}. */ +flush?(): void + +/** + * See {@link TelemetryBackend.shutdown}. + * @returns resolves when the backend's pipeline has quiesced. + */ +abstract shutdown(): Promise +``` + +Source: [`packages/session/session-telemetry/src/index.ts:140`](../../packages/session/session-telemetry/src/index.ts) + + + +### `telemetry/*` events + + + +#### `telemetry/record` — waterfall + +Transform one outbound record before it reaches the backend. This waterfall is the seam's redaction extension point. It ships NO rules of its own: the innermost `next()` passes the record through unchanged, and with no listener mounted records reach the backend as captured, so exported data is exactly as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath. Dispatched synchronously on the capture hot path inside the coordinator's containment: a throwing listener withholds that one record (fail-closed) and never reaches the agent loop. Live capture dispatches at append time; on-demand capture dispatches while reading the canonical log. Redaction applies to the exported copy only; the canonical session log is never rewritten. + +```ts cordis-catalog +/** + * Transform one outbound record before it reaches the backend. This + * waterfall is the seam's redaction extension point. It ships NO rules + * of its own: the + * innermost `next()` passes the record through unchanged, and with no + * listener mounted records reach the backend as captured, so exported + * data is exactly as clean as the rules a deployment mounts. Listeners + * stack by transforming `next()`'s return value; returning without + * `next()` replaces everything beneath. Dispatched synchronously on the + * capture hot path inside the coordinator's containment: a throwing + * listener withholds that one record (fail-closed) and never reaches the + * agent loop. Live capture dispatches at append time; on-demand capture + * dispatches while reading the canonical log. Redaction applies to the + * exported copy only; the canonical session log is never rewritten. + * @param record - the candidate record, already the coordinator's own deep + * copy; listeners return a (possibly new) record and must not mutate it. + * @mode waterfall + */ +'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord +``` + +Source: [`packages/session/session-telemetry/src/index.ts:43`](../../packages/session/session-telemetry/src/index.ts) + diff --git a/docs/subsystems/token-meter.i18n.yaml b/docs/subsystems/token-meter.i18n.yaml index 3f58d10153..e348aca7a0 100644 --- a/docs/subsystems/token-meter.i18n.yaml +++ b/docs/subsystems/token-meter.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/token-meter.md -token-meter.md: 05784e294485a11acf0e4c8972e4083b1786c943 -token-meter.zh.md: 0474d86188a111014e6d72e9962121c08a228d73 +# pnpm run verify-translation-pairing --write docs/subsystems/token-meter.md +token-meter.md: 0f53955b3e6afce84520314f4de453edaa897158 +token-meter.zh.md: 3ba22c54d599982d3f7bdbcb9160b6ffc3255e07 diff --git a/docs/subsystems/token-meter.md b/docs/subsystems/token-meter.md index 05784e2944..0f53955b3e 100644 --- a/docs/subsystems/token-meter.md +++ b/docs/subsystems/token-meter.md @@ -41,3 +41,50 @@ interface TokenSurfaceNode { ``` Surface order is authoritative; replacement nodes can have higher durable seqs than later positional nodes. The snapshot is immutable and does not grow when the underlying replay fold advances. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.tokenMeter` — `TokenMeterService` + +Replay owner for one service-wide estimator and isolated per-session folds. + +```ts cordis-catalog +/** + * Measure current request pressure and surface through the durable tail. + * + * Provider usage is reused only when the latest successful call's canonical + * request envelope matches `requestHeader` and its total is no lower than + * that call's full heuristic anchor; otherwise the complete envelope and + * surface are heuristically repriced. + * + * `requestHeader` affects request pressure only; surface fields always + * describe the current session surface. Every call clones those positional + * nodes, so measurement is O(surface). + * + * @param session - session to replay through its current durable tail. + * @param requestHeader - optional effective request envelope replacing the latest logged header. + * @returns a detached deeply immutable pressure and surface measurement. + */ +measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement + +/** + * Heuristically price one model-visible message (instance face of the pure + * `estimateMessage` export from `estimate.ts`). + * @param message - message to price without mutation. + * @returns content and role-framing tokens under the fixed service heuristic. + */ +estimateMessage(message: Message): number +``` + +Types: [EpochHeader](session.md) · [Message](core.md) · [Session](session.md) + +Source: [`packages/llm/token-meter/src/index.ts:74`](../../packages/llm/token-meter/src/index.ts) + diff --git a/docs/subsystems/token-meter.zh.md b/docs/subsystems/token-meter.zh.md index 0474d86188..3ba22c54d5 100644 --- a/docs/subsystems/token-meter.zh.md +++ b/docs/subsystems/token-meter.zh.md @@ -41,3 +41,50 @@ interface TokenSurfaceNode { ``` 表层顺序具有权威性;替换节点的持久 seq 可能高于位置排在其后的节点。该快照不可变,不会随底层回放折叠推进而增长。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.tokenMeter` — `TokenMeterService` + +Replay owner for one service-wide estimator and isolated per-session folds. + +```ts cordis-catalog +/** + * Measure current request pressure and surface through the durable tail. + * + * Provider usage is reused only when the latest successful call's canonical + * request envelope matches `requestHeader` and its total is no lower than + * that call's full heuristic anchor; otherwise the complete envelope and + * surface are heuristically repriced. + * + * `requestHeader` affects request pressure only; surface fields always + * describe the current session surface. Every call clones those positional + * nodes, so measurement is O(surface). + * + * @param session - session to replay through its current durable tail. + * @param requestHeader - optional effective request envelope replacing the latest logged header. + * @returns a detached deeply immutable pressure and surface measurement. + */ +measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement + +/** + * Heuristically price one model-visible message (instance face of the pure + * `estimateMessage` export from `estimate.ts`). + * @param message - message to price without mutation. + * @returns content and role-framing tokens under the fixed service heuristic. + */ +estimateMessage(message: Message): number +``` + +Types: [EpochHeader](session.md) · [Message](core.md) · [Session](session.md) + +Source: [`packages/llm/token-meter/src/index.ts:74`](../../packages/llm/token-meter/src/index.ts) + diff --git a/docs/subsystems/tools.i18n.yaml b/docs/subsystems/tools.i18n.yaml index a30f122038..8b0aa47c9a 100644 --- a/docs/subsystems/tools.i18n.yaml +++ b/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: 853eed16cff451edcc32bc3aa5c6bc7cabb0f518 -tools.zh.md: ec8809f6bebd185404828c5c5879f8eff832ea9a +tools.md: 5d72bd54f0dc5995218087030f24c467c8ccbb38 +tools.zh.md: 2340d4c2d92c51aa9cc89f4694b626c76d05140b diff --git a/docs/subsystems/tools.md b/docs/subsystems/tools.md index 853eed16cf..5d72bd54f0 100644 --- a/docs/subsystems/tools.md +++ b/docs/subsystems/tools.md @@ -455,3 +455,241 @@ How a tool wants its call shown in a UI (an editor tool-call card, a CLI log lin `ToolCallKind` (`'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`) picks an icon on a generic card. `FileLocation` (`{ path, line? }`), `FileDiff` (`{ path, oldText, newText }`), and `ReadFileLine` (`{ number, text }`, one 1-based numbered line of a read window) are the shared file-card vocabulary. The design is pinned in [the render-intent-union Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md); host/client runtimes project this neutral vocabulary into their own views. The full presentation field docs live in [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts). The `bash` schema and executor are on [bash.md](bash.md); generic background controls are on [tasks.md](tasks.md). + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.tools` — `ToolRegistry` + +Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch. + +```ts cordis-catalog +/** + * Register globally or in the calling agent scope. Scoped tools shadow + * globals; duplicates within one layer and the reserved `run_code` name fail. + * @param definition - tool schema, execution, and optional finalization/presentation callbacks. + * @returns the exact disposer that unregisters the tool. + */ +register(definition: ToolDefinition): () => void + +/** + * Restrict global tools for the calling agent scope. Empty filters, unknown + * names, scope-local names, and reserved transport names fail. Restrictions + * intersect; scoped registrations remain visible. + * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove). + * @returns the exact disposer that lifts this restriction. + */ +restrict(filter: ToolRestriction): () => void + +/** + * Register a monotonic guard after the extensible `tools/pre-execute` + * waterfall. A plain-context guard applies globally; one registered through + * `agent.ctx` applies only to that agent. Any matching guard may deny by + * returning a reason, while no guard can force-allow a call another guard + * denied. The exact effect disposer is returned for ordered ownership and + * HMR cleanup. + * @param guard - synchronous check; a returned string denies the execution. + * @returns the exact disposer that unregisters the guard. + */ +guard(guard: ToolGuard): () => void + +/** + * Look up a tool as one scope sees it (scoped + * shadows global; a restricted-away global reads as absent). Presenters pass + * the calling agent so the rendered card matches the definition that + * actually executed. + * @param name - the tool name as registered. + * @param scope - the viewing scope (the agent); omitted = the global view. + * @returns the definition the scope resolves, or undefined when none is visible. + */ +get(name: string, scope?: ScopeKey): ToolDefinition | undefined + +/** + * Project visible definitions onto the allowlisted model-facing schema fields, + * excluding execution and presentation callbacks. + * @param scope - the viewing scope (the agent); omitted = the global view. + * @returns one deep-cloned schema per visible tool. + */ +schemas(scope?: ScopeKey): ToolSchema[] + +/** + * Classify a pending call through the caller's visible tool definition. Only + * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or + * throwing classifiers are exclusive. + * @param exec - call name, parsed arguments, and optional agent scope. + * @returns the fail-closed scheduling mode. + */ +executionMode(exec: ToolExecutionInput): ToolExecutionMode + +/** + * Execute through pre-policy, guards, around-dispatch, post-policy, + * definition-owned content finalization, and final notification. Tool and + * listener failures resolve as materialized error results; an invisible tool + * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen + * snapshot final observers receive. Cancellation + * arriving after entry and before final result materialization skips a + * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a + * successful started outcome with `ABORTED`; already-started work is still + * drained and may retain a tool-owned structured error. + * @param exec - the typed same-process call input. The registry assigns its + * correlation token before policy begins. + * @returns the materialized final result. + */ +async execute(exec: ToolExecutionInput): Promise +``` + +Types: [ScopeKey](scope.md) + +Source: [`packages/core/tools/src/index.ts:739`](../../packages/core/tools/src/index.ts) + + + +### `tools/*` events + + + +#### `tools/change` — emit + +A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's. + +```ts cordis-catalog +/** + * A tool was registered or unregistered, or a scoped restriction changed + * (the available tool set changed — possibly for one scope only). An + * UNFILTERED registry-subject notification, deliberately not scope-filtered + * dispatch: a global change concerns every agent's next assembly, so a + * scoped listener subscribing here sees every change, not just its own + * scope's. + * @mode emit + */ +'tools/change'(): void +``` + +Source: [`packages/core/tools/src/index.ts:191`](../../packages/core/tools/src/index.ts) + + + +#### `tools/code-dispatch-log` — waterfall + +Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy's preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the unshaped content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. + +```ts cordis-catalog +/** + * Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before + * the bridge appends its `tool/code-dispatch` event. `next()` keeps the + * content unchanged; a listener may return replacement blocks (e.g. the + * spill policy's preview + locator for an oversized text result). Only the + * logged copy is affected — the program already received the complete + * value, and the model sees neither. A throwing listener is contained: + * the bridge falls back to logging the unshaped content. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. + * @param dispatch - the parent execution, sub-call identity, and the settled content to log. + * @mode waterfall + */ +'tools/code-dispatch-log'(this: Scoped, dispatch: CodeDispatchLog, next: () => Promise): Promise +``` + +Types: [ContentBlock](core.md) · [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:173`](../../packages/core/tools/src/index.ts) + + + +#### `tools/execute` — waterfall + +Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a normalized result; wrappers may change only `exec.signal`, while call identity remains immutable. The registry re-fuses the original caller signal before the body, so replacement cannot detach caller cancellation; wrappers must still restore their signal and reach quiescence. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + +```ts cordis-catalog +/** + * Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns + * a normalized result; wrappers may change only `exec.signal`, while call + * identity remains immutable. The registry re-fuses the original caller + * signal before the body, so replacement cannot detach caller cancellation; + * wrappers must still restore their signal and reach quiescence. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + * @param exec - the allowed call about to dispatch (name, parsed arguments, caller agent, signal). + * @mode waterfall + */ +'tools/execute'(this: Scoped, exec: ToolDispatchExecution, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:148`](../../packages/core/tools/src/index.ts) + + + +#### `tools/post-execute` — waterfall + +Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Async listeners must observe `exec.signal`; after they settle, caller cancellation replaces only a successful accepted outcome with the code selected by whether the tool body was invoked. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + +```ts cordis-catalog +/** + * Accept, replace, enrich, or block a normalized dispatch result. `next()` + * accepts it unchanged; thrown tools still reach this seam as errors. Async + * listeners must observe `exec.signal`; after they settle, caller + * cancellation replaces only a successful accepted outcome with the code + * selected by whether the tool body was invoked. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + * @param exec - the call that just ran (name, parsed arguments, caller agent). + * @param result - the dispatch outcome a listener may accept, replace, or block. + * @mode waterfall + */ +'tools/post-execute'(this: Scoped, exec: ToolExecution, result: Readonly, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:160`](../../packages/core/tools/src/index.ts) + + + +#### `tools/pre-execute` — waterfall + +Allow, deny, or ask before dispatch. `next()` delegates to allow; missing approval support turns `ask` into denial. Async gates must observe `exec.signal`; the registry rechecks cancellation after they settle but never abandons their promise. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + +```ts cordis-catalog +/** + * Allow, deny, or ask before dispatch. `next()` delegates to allow; missing + * approval support turns `ask` into denial. Async gates must observe + * `exec.signal`; the registry rechecks cancellation after they settle but + * never abandons their promise. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + * @param exec - the pending call (name, parsed arguments, caller agent). + * @mode waterfall + */ +'tools/pre-execute'(this: Scoped, exec: ToolExecution, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:137`](../../packages/core/tools/src/index.ts) + + + +#### `tools/result` — emit + +Observe the frozen, lossless-JSON final outcome. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. + +```ts cordis-catalog +/** + * Observe the frozen, lossless-JSON final outcome. Listener failures are contained. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. + * @param exec - the execution object that traversed the pipeline. + * @param result - a deep-frozen snapshot of the final returned result. + * @mode emit + */ +'tools/result'(this: Scoped, exec: Readonly, result: Readonly): undefined +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:181`](../../packages/core/tools/src/index.ts) + diff --git a/docs/subsystems/tools.zh.md b/docs/subsystems/tools.zh.md index ec8809f6be..2340d4c2d9 100644 --- a/docs/subsystems/tools.zh.md +++ b/docs/subsystems/tools.zh.md @@ -455,3 +455,241 @@ type ObjectJsonSchema = JsonSchemaNode & { type: 'object' } `ToolCallKind`(`'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`)用于为通用卡片选择图标。`FileLocation`(`{ path, line? }`)、`FileDiff`(`{ path, oldText, newText }`)与 `ReadFileLine`(`{ number, text }`,读取窗口中一行带 1-based 行号的内容)是共享的文件卡片词汇。该设计由[渲染意图联合类型 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md)固定;host/client 运行时将这套中性词汇投影为各自的视图。 完整的展示字段文档见 [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts)。`bash` schema 与执行器见 [bash.md](bash.md);通用后台控制见 [tasks.md](tasks.md)。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.tools` — `ToolRegistry` + +Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch. + +```ts cordis-catalog +/** + * Register globally or in the calling agent scope. Scoped tools shadow + * globals; duplicates within one layer and the reserved `run_code` name fail. + * @param definition - tool schema, execution, and optional finalization/presentation callbacks. + * @returns the exact disposer that unregisters the tool. + */ +register(definition: ToolDefinition): () => void + +/** + * Restrict global tools for the calling agent scope. Empty filters, unknown + * names, scope-local names, and reserved transport names fail. Restrictions + * intersect; scoped registrations remain visible. + * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove). + * @returns the exact disposer that lifts this restriction. + */ +restrict(filter: ToolRestriction): () => void + +/** + * Register a monotonic guard after the extensible `tools/pre-execute` + * waterfall. A plain-context guard applies globally; one registered through + * `agent.ctx` applies only to that agent. Any matching guard may deny by + * returning a reason, while no guard can force-allow a call another guard + * denied. The exact effect disposer is returned for ordered ownership and + * HMR cleanup. + * @param guard - synchronous check; a returned string denies the execution. + * @returns the exact disposer that unregisters the guard. + */ +guard(guard: ToolGuard): () => void + +/** + * Look up a tool as one scope sees it (scoped + * shadows global; a restricted-away global reads as absent). Presenters pass + * the calling agent so the rendered card matches the definition that + * actually executed. + * @param name - the tool name as registered. + * @param scope - the viewing scope (the agent); omitted = the global view. + * @returns the definition the scope resolves, or undefined when none is visible. + */ +get(name: string, scope?: ScopeKey): ToolDefinition | undefined + +/** + * Project visible definitions onto the allowlisted model-facing schema fields, + * excluding execution and presentation callbacks. + * @param scope - the viewing scope (the agent); omitted = the global view. + * @returns one deep-cloned schema per visible tool. + */ +schemas(scope?: ScopeKey): ToolSchema[] + +/** + * Classify a pending call through the caller's visible tool definition. Only + * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or + * throwing classifiers are exclusive. + * @param exec - call name, parsed arguments, and optional agent scope. + * @returns the fail-closed scheduling mode. + */ +executionMode(exec: ToolExecutionInput): ToolExecutionMode + +/** + * Execute through pre-policy, guards, around-dispatch, post-policy, + * definition-owned content finalization, and final notification. Tool and + * listener failures resolve as materialized error results; an invisible tool + * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen + * snapshot final observers receive. Cancellation + * arriving after entry and before final result materialization skips a + * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a + * successful started outcome with `ABORTED`; already-started work is still + * drained and may retain a tool-owned structured error. + * @param exec - the typed same-process call input. The registry assigns its + * correlation token before policy begins. + * @returns the materialized final result. + */ +async execute(exec: ToolExecutionInput): Promise +``` + +Types: [ScopeKey](scope.md) + +Source: [`packages/core/tools/src/index.ts:739`](../../packages/core/tools/src/index.ts) + + + +### `tools/*` events + + + +#### `tools/change` — emit + +A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's. + +```ts cordis-catalog +/** + * A tool was registered or unregistered, or a scoped restriction changed + * (the available tool set changed — possibly for one scope only). An + * UNFILTERED registry-subject notification, deliberately not scope-filtered + * dispatch: a global change concerns every agent's next assembly, so a + * scoped listener subscribing here sees every change, not just its own + * scope's. + * @mode emit + */ +'tools/change'(): void +``` + +Source: [`packages/core/tools/src/index.ts:191`](../../packages/core/tools/src/index.ts) + + + +#### `tools/code-dispatch-log` — waterfall + +Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy's preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the unshaped content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. + +```ts cordis-catalog +/** + * Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before + * the bridge appends its `tool/code-dispatch` event. `next()` keeps the + * content unchanged; a listener may return replacement blocks (e.g. the + * spill policy's preview + locator for an oversized text result). Only the + * logged copy is affected — the program already received the complete + * value, and the model sees neither. A throwing listener is contained: + * the bridge falls back to logging the unshaped content. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. + * @param dispatch - the parent execution, sub-call identity, and the settled content to log. + * @mode waterfall + */ +'tools/code-dispatch-log'(this: Scoped, dispatch: CodeDispatchLog, next: () => Promise): Promise +``` + +Types: [ContentBlock](core.md) · [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:173`](../../packages/core/tools/src/index.ts) + + + +#### `tools/execute` — waterfall + +Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a normalized result; wrappers may change only `exec.signal`, while call identity remains immutable. The registry re-fuses the original caller signal before the body, so replacement cannot detach caller cancellation; wrappers must still restore their signal and reach quiescence. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + +```ts cordis-catalog +/** + * Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns + * a normalized result; wrappers may change only `exec.signal`, while call + * identity remains immutable. The registry re-fuses the original caller + * signal before the body, so replacement cannot detach caller cancellation; + * wrappers must still restore their signal and reach quiescence. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + * @param exec - the allowed call about to dispatch (name, parsed arguments, caller agent, signal). + * @mode waterfall + */ +'tools/execute'(this: Scoped, exec: ToolDispatchExecution, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:148`](../../packages/core/tools/src/index.ts) + + + +#### `tools/post-execute` — waterfall + +Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Async listeners must observe `exec.signal`; after they settle, caller cancellation replaces only a successful accepted outcome with the code selected by whether the tool body was invoked. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + +```ts cordis-catalog +/** + * Accept, replace, enrich, or block a normalized dispatch result. `next()` + * accepts it unchanged; thrown tools still reach this seam as errors. Async + * listeners must observe `exec.signal`; after they settle, caller + * cancellation replaces only a successful accepted outcome with the code + * selected by whether the tool body was invoked. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + * @param exec - the call that just ran (name, parsed arguments, caller agent). + * @param result - the dispatch outcome a listener may accept, replace, or block. + * @mode waterfall + */ +'tools/post-execute'(this: Scoped, exec: ToolExecution, result: Readonly, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:160`](../../packages/core/tools/src/index.ts) + + + +#### `tools/pre-execute` — waterfall + +Allow, deny, or ask before dispatch. `next()` delegates to allow; missing approval support turns `ask` into denial. Async gates must observe `exec.signal`; the registry rechecks cancellation after they settle but never abandons their promise. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + +```ts cordis-catalog +/** + * Allow, deny, or ask before dispatch. `next()` delegates to allow; missing + * approval support turns `ask` into denial. Async gates must observe + * `exec.signal`; the registry rechecks cancellation after they settle but + * never abandons their promise. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + * @param exec - the pending call (name, parsed arguments, caller agent). + * @mode waterfall + */ +'tools/pre-execute'(this: Scoped, exec: ToolExecution, next: () => Promise): Promise +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:137`](../../packages/core/tools/src/index.ts) + + + +#### `tools/result` — emit + +Observe the frozen, lossless-JSON final outcome. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. + +```ts cordis-catalog +/** + * Observe the frozen, lossless-JSON final outcome. Listener failures are contained. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. + * @param exec - the execution object that traversed the pipeline. + * @param result - a deep-frozen snapshot of the final returned result. + * @mode emit + */ +'tools/result'(this: Scoped, exec: Readonly, result: Readonly): undefined +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/tools/src/index.ts:181`](../../packages/core/tools/src/index.ts) + diff --git a/docs/subsystems/typert.i18n.yaml b/docs/subsystems/typert.i18n.yaml index 75b7837687..b8fdb092f2 100644 --- a/docs/subsystems/typert.i18n.yaml +++ b/docs/subsystems/typert.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/typert.md -typert.md: c70e50e2fea8455eb75dfdf8c309f659ab9cb2f9 -typert.zh.md: 2cd1636d4cc8dbcfa009073b4a8e1dcc8d5897e4 +# pnpm run verify-translation-pairing --write docs/subsystems/typert.md +typert.md: 8ce52837ceee8e9c8c4a61de28f972912faa2cf0 +typert.zh.md: ff6b45a517c75392ee87fa348d72bfae6cdf1efb diff --git a/docs/subsystems/typert.md b/docs/subsystems/typert.md index c70e50e2fe..8ce52837ce 100644 --- a/docs/subsystems/typert.md +++ b/docs/subsystems/typert.md @@ -201,3 +201,96 @@ interface TypeRTClientRemote extends TypeRTRemoteNamespaceMap { $mount(contribution: TypeRTRemoteContribution): Promise } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.typert` — `TypertRegistry` + +Registry of generated schemas, package reflection, invocations, and Remote dependency providers. + +```ts cordis-catalog +/** + * Register one generated contribution atomically for the calling fiber. + * Duplicate package-face identities, schemas, invocation ids, or endpoints + * reject the whole batch. + * @param contribution - generated schemas, reflection, and Host invocations. + * @returns the exact effect disposer that removes this contribution. + */ +register(contribution: TypertContribution): TypeRTDisposer + +/** + * Look up one schema by `#`. + * @param key - global schema key. + * @returns the live schema record, or `undefined` when absent. + */ +get(key: string): TypertSchemaRecord | undefined + +/** + * Resolve one required schema. + * @param key - global schema key. + * @returns the live schema record. + * @throws when the key is malformed, the package face is absent, or the schema is not contributed. + */ +resolve(key: string): TypertSchemaRecord + +/** + * Enumerate live schemas in registration order. + * @param filter - optional package and face restriction. + * @returns matching schema records. + */ +list(filter: TypertSchemaFilter = {}): TypertSchemaRecord[] + +/** + * Look up generated reflection for one package face. + * @param packageName - exact npm package name. + * @param face - face to query; defaults to the host runtime. + * @returns the live package record, or `undefined` when absent. + */ +getPackage(packageName: string, face: TypertFace = 'host'): TypertPackageRecord | undefined + +/** + * Enumerate generated package reflection in registration order. + * @param filter - optional package and face restriction. + * @returns matching package records. + */ +listPackages(filter: TypertPackageFilter = {}): TypertPackageRecord[] + +/** + * Project a live Zod schema to JSON Schema without caching the result. + * @param key - global schema key. + * @param params - Zod projection parameters. + * @returns a fresh JSON Schema document. + */ +toJSONSchema(key: string, params?: z.core.ToJSONSchemaParams): z.core.JSONSchema.BaseSchema +``` + +Types: [TypertContribution](invariants.md) · [TypertFace](invariants.md) · [TypertPackageFilter](invariants.md) · [TypertPackageRecord](invariants.md) · [TypertSchemaFilter](invariants.md) · [TypertSchemaRecord](invariants.md) + +Source: [`packages/typert/registry/src/service.ts:446`](../../packages/typert/registry/src/service.ts) + + + +### `ctx.typertGateway` — `TypertGatewayService` + +Resolve strict generated definitions or conservative SRC markers against current Cordis Services and TypeRT providers. + +```ts cordis-catalog +/** + * Invoke one live Remote method through strict generated reflection or SRC markers. + * @param request - decoded endpoint and exact named wire arguments. + * @returns the validated business result. + * @throws {@link TypertGatewayError} for dispatch, provider, or boundary failures; lookup-policy and business errors retain identity. + */ +async invoke(request: InvokeRemoteRequest): Promise +``` + +Source: [`packages/api/gateway/src/index.ts:78`](../../packages/api/gateway/src/index.ts) + diff --git a/docs/subsystems/typert.zh.md b/docs/subsystems/typert.zh.md index 2cd1636d4c..ff6b45a517 100644 --- a/docs/subsystems/typert.zh.md +++ b/docs/subsystems/typert.zh.md @@ -201,3 +201,96 @@ interface TypeRTClientRemote extends TypeRTRemoteNamespaceMap { $mount(contribution: TypeRTRemoteContribution): Promise } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.typert` — `TypertRegistry` + +Registry of generated schemas, package reflection, invocations, and Remote dependency providers. + +```ts cordis-catalog +/** + * Register one generated contribution atomically for the calling fiber. + * Duplicate package-face identities, schemas, invocation ids, or endpoints + * reject the whole batch. + * @param contribution - generated schemas, reflection, and Host invocations. + * @returns the exact effect disposer that removes this contribution. + */ +register(contribution: TypertContribution): TypeRTDisposer + +/** + * Look up one schema by `#`. + * @param key - global schema key. + * @returns the live schema record, or `undefined` when absent. + */ +get(key: string): TypertSchemaRecord | undefined + +/** + * Resolve one required schema. + * @param key - global schema key. + * @returns the live schema record. + * @throws when the key is malformed, the package face is absent, or the schema is not contributed. + */ +resolve(key: string): TypertSchemaRecord + +/** + * Enumerate live schemas in registration order. + * @param filter - optional package and face restriction. + * @returns matching schema records. + */ +list(filter: TypertSchemaFilter = {}): TypertSchemaRecord[] + +/** + * Look up generated reflection for one package face. + * @param packageName - exact npm package name. + * @param face - face to query; defaults to the host runtime. + * @returns the live package record, or `undefined` when absent. + */ +getPackage(packageName: string, face: TypertFace = 'host'): TypertPackageRecord | undefined + +/** + * Enumerate generated package reflection in registration order. + * @param filter - optional package and face restriction. + * @returns matching package records. + */ +listPackages(filter: TypertPackageFilter = {}): TypertPackageRecord[] + +/** + * Project a live Zod schema to JSON Schema without caching the result. + * @param key - global schema key. + * @param params - Zod projection parameters. + * @returns a fresh JSON Schema document. + */ +toJSONSchema(key: string, params?: z.core.ToJSONSchemaParams): z.core.JSONSchema.BaseSchema +``` + +Types: [TypertContribution](invariants.md) · [TypertFace](invariants.md) · [TypertPackageFilter](invariants.md) · [TypertPackageRecord](invariants.md) · [TypertSchemaFilter](invariants.md) · [TypertSchemaRecord](invariants.md) + +Source: [`packages/typert/registry/src/service.ts:446`](../../packages/typert/registry/src/service.ts) + + + +### `ctx.typertGateway` — `TypertGatewayService` + +Resolve strict generated definitions or conservative SRC markers against current Cordis Services and TypeRT providers. + +```ts cordis-catalog +/** + * Invoke one live Remote method through strict generated reflection or SRC markers. + * @param request - decoded endpoint and exact named wire arguments. + * @returns the validated business result. + * @throws {@link TypertGatewayError} for dispatch, provider, or boundary failures; lookup-policy and business errors retain identity. + */ +async invoke(request: InvokeRemoteRequest): Promise +``` + +Source: [`packages/api/gateway/src/index.ts:78`](../../packages/api/gateway/src/index.ts) + diff --git a/docs/subsystems/user-interaction.i18n.yaml b/docs/subsystems/user-interaction.i18n.yaml index 80ebb810c8..342cf9b9f7 100644 --- a/docs/subsystems/user-interaction.i18n.yaml +++ b/docs/subsystems/user-interaction.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/user-interaction.md -user-interaction.md: ec22eb28e9554d6454bf2670f73f2e9b199df014 -user-interaction.zh.md: 814f8911e41aecf568f627f76f8239bac4ea719b +user-interaction.md: a19155ae06af0133ae468c004b2e3b66f1de3fb8 +user-interaction.zh.md: dc5845a976068981b2761e26502eaf7dd95dcb01 diff --git a/docs/subsystems/user-interaction.md b/docs/subsystems/user-interaction.md index ec22eb28e9..a19155ae06 100644 --- a/docs/subsystems/user-interaction.md +++ b/docs/subsystems/user-interaction.md @@ -132,3 +132,47 @@ class UserInteractionError extends HarnessError { } } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.userInteraction` — `UserInteractionService` + +`ctx.userInteraction`: one active UI provider plus an `ask()` surface. + +```ts cordis-catalog +/** + * Register the UI provider. Only one provider may be active in a context. + * + * @param provider UI-side implementation that collects answers. + * @returns Disposer that unregisters this provider. + */ +registerProvider(provider: UserInteractionProvider): () => void + +/** + * Ask the active UI provider and wait for the user's answer. + * + * When a caller supplies an agent, human interaction is valid only for the + * exact live runtime root. Runtime ownership, not durable session lineage, + * decides this boundary: an owned child has no human answerer and would + * block forever, while a lineage-bearing session resumed as a new runtime + * root may ask normally. + * + * @param request Questions, owner agent, and abort signal. + * @returns The answer chosen or typed by the human. + * @throws {UserInteractionError} code `CALLER_NOT_LIVE` when a supplied + * agent is not the registry's exact live instance, or `DELEGATED_CALLER` + * when that live agent is owned by another agent. + */ +async ask(request: AskUserQuestionRequest): Promise +``` + +Source: [`packages/interaction/user-interaction/src/index.ts:51`](../../packages/interaction/user-interaction/src/index.ts) + diff --git a/docs/subsystems/user-interaction.zh.md b/docs/subsystems/user-interaction.zh.md index 814f8911e4..dc5845a976 100644 --- a/docs/subsystems/user-interaction.zh.md +++ b/docs/subsystems/user-interaction.zh.md @@ -132,3 +132,47 @@ class UserInteractionError extends HarnessError { } } ``` + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.userInteraction` — `UserInteractionService` + +`ctx.userInteraction`: one active UI provider plus an `ask()` surface. + +```ts cordis-catalog +/** + * Register the UI provider. Only one provider may be active in a context. + * + * @param provider UI-side implementation that collects answers. + * @returns Disposer that unregisters this provider. + */ +registerProvider(provider: UserInteractionProvider): () => void + +/** + * Ask the active UI provider and wait for the user's answer. + * + * When a caller supplies an agent, human interaction is valid only for the + * exact live runtime root. Runtime ownership, not durable session lineage, + * decides this boundary: an owned child has no human answerer and would + * block forever, while a lineage-bearing session resumed as a new runtime + * root may ask normally. + * + * @param request Questions, owner agent, and abort signal. + * @returns The answer chosen or typed by the human. + * @throws {UserInteractionError} code `CALLER_NOT_LIVE` when a supplied + * agent is not the registry's exact live instance, or `DELEGATED_CALLER` + * when that live agent is owned by another agent. + */ +async ask(request: AskUserQuestionRequest): Promise +``` + +Source: [`packages/interaction/user-interaction/src/index.ts:51`](../../packages/interaction/user-interaction/src/index.ts) + diff --git a/docs/subsystems/web.i18n.yaml b/docs/subsystems/web.i18n.yaml index e8ae658d3a..f3f0ab9758 100644 --- a/docs/subsystems/web.i18n.yaml +++ b/docs/subsystems/web.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/web.md -web.md: 20d07240c9d9fea2f1f5abbac810f349a3e81f9b -web.zh.md: e2982ba571353752e3a7d10130599e8c8fe941f8 +# pnpm run verify-translation-pairing --write docs/subsystems/web.md +web.md: f300bf2daef172fe4a69c00f36c804cf874c189d +web.zh.md: 0c72e343c1b6ea5de8e7c296014da35b0291a33b diff --git a/docs/subsystems/web.md b/docs/subsystems/web.md index 20d07240c9..f300bf2dae 100644 --- a/docs/subsystems/web.md +++ b/docs/subsystems/web.md @@ -133,3 +133,70 @@ Selection never depends on registration, config, or HMR order: a capability has ## The service `WebService` registers search and fetch providers, rejects duplicate ids with `WEB_DUPLICATE_PROVIDER`, and resolves providers at execution time with structured selection errors. The local fetch backend accepts only HTTP(S), rejects credentials, caps redirects, bytes, characters, and time, revalidates every same-origin redirect hop, and decodes the body; the tool owns presentation. Private-network blocking is deferred, so do not enable `web_fetch` where it can reach sensitive internal targets. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.web` — `WebService` + +The web access service. Registered as `ctx.web` (one instance per context). + +Selection semantics (resolved at execution time, never order-dependent): + +- A configured id that is registered and `available()` → that provider. +- A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`. +- A configured id registered but unavailable → `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. +- No id configured, exactly one registered usable provider → that provider. +- No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`. +- No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`. + +```ts cordis-catalog +/** + * Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` + * if its id is already registered for search. Returns a disposer; disposed + * with the calling fiber. + * @param provider - the provider; its `id` is the registry key. + * @returns the disposer that unregisters the provider. + */ +registerSearchProvider(provider: WebSearchProvider): () => void + +/** + * Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` + * if its id is already registered for fetch. Returns a disposer; disposed + * with the calling fiber. + * @param provider - the provider; its `id` is the registry key. + * @returns the disposer that unregisters the provider. + */ +registerFetchProvider(provider: WebFetchProvider): () => void + +/** + * Run one search through the selected provider. Resolves the provider at call + * time with the selection rules above; throws {@link WebError} when the + * capability cannot run. The seam enforces `request.maxResults` on the result: + * if the provider over-returns, `sources[]` is truncated and `truncated` set. + * @param request - the query plus result-shaping options. + * @param signal - optional cancellation signal forwarded to the provider. + * @returns the provider's results, capped to `request.maxResults`. + */ +async search(request: WebSearchRequest, signal?: AbortSignal): Promise + +/** + * Retrieve one URL through the selected provider. Resolves the provider at + * call time with the selection rules above; throws {@link WebError} when the + * capability cannot run. A non-2xx response is a result, not a throw. + * @param request - the URL plus retrieval options. + * @param signal - optional cancellation signal forwarded to the provider. + * @returns the retrieval outcome; non-2xx responses resolve descriptively. + */ +async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise +``` + +Source: [`packages/web/web/src/index.ts:74`](../../packages/web/web/src/index.ts) + diff --git a/docs/subsystems/web.zh.md b/docs/subsystems/web.zh.md index e2982ba571..0c72e343c1 100644 --- a/docs/subsystems/web.zh.md +++ b/docs/subsystems/web.zh.md @@ -133,3 +133,70 @@ type WebFetchBody = ## 服务 `WebService` 注册搜索与抓取提供方,以 `WEB_DUPLICATE_PROVIDER` 拒绝重复 id,并在执行时以结构化的选择错误解析提供方。本地抓取后端仅接受 HTTP(S)、拒绝凭证、限制重定向次数、字节数、字符数和时间、对每一跳同源重定向重新校验,并解码正文;展示由工具负责。SSRF/私有网络防护尚未实现,因此在能够触及敏感内部目标的环境中,禁止启用 `web_fetch`。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.web` — `WebService` + +The web access service. Registered as `ctx.web` (one instance per context). + +Selection semantics (resolved at execution time, never order-dependent): + +- A configured id that is registered and `available()` → that provider. +- A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`. +- A configured id registered but unavailable → `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. +- No id configured, exactly one registered usable provider → that provider. +- No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`. +- No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`. + +```ts cordis-catalog +/** + * Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` + * if its id is already registered for search. Returns a disposer; disposed + * with the calling fiber. + * @param provider - the provider; its `id` is the registry key. + * @returns the disposer that unregisters the provider. + */ +registerSearchProvider(provider: WebSearchProvider): () => void + +/** + * Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` + * if its id is already registered for fetch. Returns a disposer; disposed + * with the calling fiber. + * @param provider - the provider; its `id` is the registry key. + * @returns the disposer that unregisters the provider. + */ +registerFetchProvider(provider: WebFetchProvider): () => void + +/** + * Run one search through the selected provider. Resolves the provider at call + * time with the selection rules above; throws {@link WebError} when the + * capability cannot run. The seam enforces `request.maxResults` on the result: + * if the provider over-returns, `sources[]` is truncated and `truncated` set. + * @param request - the query plus result-shaping options. + * @param signal - optional cancellation signal forwarded to the provider. + * @returns the provider's results, capped to `request.maxResults`. + */ +async search(request: WebSearchRequest, signal?: AbortSignal): Promise + +/** + * Retrieve one URL through the selected provider. Resolves the provider at + * call time with the selection rules above; throws {@link WebError} when the + * capability cannot run. A non-2xx response is a result, not a throw. + * @param request - the URL plus retrieval options. + * @param signal - optional cancellation signal forwarded to the provider. + * @returns the retrieval outcome; non-2xx responses resolve descriptively. + */ +async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise +``` + +Source: [`packages/web/web/src/index.ts:74`](../../packages/web/web/src/index.ts) + diff --git a/docs/subsystems/workflow.i18n.yaml b/docs/subsystems/workflow.i18n.yaml index 7f996a1c8e..4f581d8bae 100644 --- a/docs/subsystems/workflow.i18n.yaml +++ b/docs/subsystems/workflow.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/core-data-structures/workflow.md -workflow.md: 8d271b89e71de6f6bef548aa8da61402ef9ada6e -workflow.zh.md: 91a4902bbc004e911c8aa84adb6a4abeda9dd59f +# pnpm run verify-translation-pairing --write docs/subsystems/workflow.md +workflow.md: 53cb8be2c975fba396c3d2023c60fc1da2ed9c8a +workflow.zh.md: 652b68c833f7f20fe72307946d02f89558be2c2b diff --git a/docs/subsystems/workflow.md b/docs/subsystems/workflow.md index 8d271b89e7..53cb8be2c9 100644 --- a/docs/subsystems/workflow.md +++ b/docs/subsystems/workflow.md @@ -129,4 +129,154 @@ Hook misuse inside a script — bad arguments, unknown/deferred `agent()` option ## Events -The `workflow/*` events (`workflow/start`, `workflow/phase`, `workflow/log`, `workflow/agent-start`, `workflow/agent-end`, `workflow/end` — see the [events catalog](../cordis-catalog/events.md)) are **observe-only** emits carrying DATA SNAPSHOTS: every payload starts with `WorkflowRunInfo` (id + meta), never the live `WorkflowRun`, so a subscriber cannot gain `cancel`/`dispose`, and `workflow/end` deliberately omits the result value (a listener observing outcomes must not receive a mutable alias of the caller's result). Every emit is per-listener contained — a throwing subscriber is logged, never propagated, and cannot starve the listeners registered after it — and every listener receives its own payload clone, so mutating it corrupts neither the engine nor other listeners; the containment mirrors `subagent/start`/`subagent/end`. +The `workflow/*` events (`workflow/start`, `workflow/phase`, `workflow/log`, `workflow/agent-start`, `workflow/agent-end`, `workflow/end` — see the [events catalog](#cordis-surface)) are **observe-only** emits carrying DATA SNAPSHOTS: every payload starts with `WorkflowRunInfo` (id + meta), never the live `WorkflowRun`, so a subscriber cannot gain `cancel`/`dispose`, and `workflow/end` deliberately omits the result value (a listener observing outcomes must not receive a mutable alias of the caller's result). Every emit is per-listener contained — a throwing subscriber is logged, never propagated, and cannot starve the listeners registered after it — and every listener receives its own payload clone, so mutating it corrupts neither the engine nor other listeners; the containment mirrors `subagent/start`/`subagent/end`. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.workflows` — `WorkflowService` (abstract seam) + +Workflow execution seam. Invalid requests throw before publication; a live run is holder-owned, its result never rejects, cancellation and disposal are bounded, and disposal waits for child cleanup within that bound. Lifecycle listener failures are contained, and `workflow/end` fires exactly once as the result settles. + +```ts cordis-catalog +/** + * Parse and execute a workflow script. + * @param request - the script, its `args`, the parent agent, and an + * optional cancel signal. + * @returns the live run; its `result` resolves when the script settles. + */ +abstract start(request: WorkflowStartRequest): WorkflowRun +``` + +Source: [`packages/workflow/workflow/src/index.ts:159`](../../packages/workflow/workflow/src/index.ts) + + + +### `workflow/*` events + + + +#### `workflow/agent-end` — emit + +One `agent()` call settled (clean result, child failure, or run cancellation). Paired with Events['workflow/agent-start'] by `agent.seq`, exactly once per started call on every stop path — on an engine termination path (a worker killed past its grace) the end is engine-synthesized with outcome `'cancelled'`. + +```ts cordis-catalog +/** + * One `agent()` call settled (clean result, child failure, or run + * cancellation). Paired with {@link Events['workflow/agent-start']} by + * `agent.seq`, exactly once per started call on every stop path — on an + * engine termination path (a worker killed past its grace) the end is + * engine-synthesized with outcome `'cancelled'`. + * @param info - the run's identity snapshot. + * @param agent - the call identity plus its outcome. + * @mode emit + */ +'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:81`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/agent-start` — emit + +One `agent()` call established a published child run. Paired with Events['workflow/agent-end'] by `agent.seq`. A call that never receives a published run from the provider emits neither event in this pair. + +```ts cordis-catalog +/** + * One `agent()` call established a published child run. Paired with + * {@link Events['workflow/agent-end']} by `agent.seq`. A call that never + * receives a published run from the provider emits neither + * event in this pair. + * @param info - the run's identity snapshot. + * @param agent - the call's sequence number, label, phase, and child id. + * @mode emit + */ +'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:70`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/end` — emit + +A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves. Paired with Events['workflow/start']. + +```ts cordis-catalog +/** + * A workflow run settled (any stop reason). Fired when + * {@link WorkflowRun.result} resolves. Paired with + * {@link Events['workflow/start']}. + * @param info - the run's identity snapshot. + * @param result - the outcome data (stop reason, error, agent count) — + * deliberately WITHOUT the result value (see {@link WorkflowResultInfo}). + * @mode emit + */ +'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:91`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/log` — emit + +The script emitted a narration line (a `log(message)` call). + +```ts cordis-catalog +/** + * The script emitted a narration line (a `log(message)` call). + * @param info - the run's identity snapshot. + * @param message - the logged message, verbatim. + * @mode emit + */ +'workflow/log'(info: WorkflowRunInfo, message: string): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:60`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/phase` — emit + +The script entered a phase (a `phase(title)` call) — progress grouping for observers; no execution semantics. + +```ts cordis-catalog +/** + * The script entered a phase (a `phase(title)` call) — progress grouping + * for observers; no execution semantics. + * @param info - the run's identity snapshot. + * @param title - the phase title, verbatim. + * @mode emit + */ +'workflow/phase'(info: WorkflowRunInfo, title: string): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:53`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/start` — emit + +A workflow run started — the script's meta block validated, the body about to execute. Paired with Events['workflow/end']. + +```ts cordis-catalog +/** + * A workflow run started — the script's meta block validated, the body + * about to execute. Paired with {@link Events['workflow/end']}. + * @param info - the run's identity snapshot (id + meta). + * @mode emit + */ +'workflow/start'(info: WorkflowRunInfo): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:45`](../../packages/workflow/workflow/src/index.ts) + diff --git a/docs/subsystems/workflow.zh.md b/docs/subsystems/workflow.zh.md index 91a4902bbc..652b68c833 100644 --- a/docs/subsystems/workflow.zh.md +++ b/docs/subsystems/workflow.zh.md @@ -129,4 +129,154 @@ interface WorkflowRun { ## 事件 -`workflow/*` 事件(`workflow/start`、`workflow/phase`、`workflow/log`、`workflow/agent-start`、`workflow/agent-end`、`workflow/end`,见[事件目录](../cordis-catalog/events.md))是**仅供观察**的 emit,携带数据快照:每个 payload 以 `WorkflowRunInfo`(id + meta)开头,而非活跃的 `WorkflowRun`,因此订阅者无法获得 `cancel`/`dispose`;`workflow/end` 刻意省略 result value(观察结果的监听器不得收到调用方 result 的可变别名)。每次 emit 对每个监听器隔离:抛出异常的订阅者被记录日志但不传播,不会饿死在它之后注册的监听器;每个监听器收到自己的 payload 克隆,因此修改它既不会损坏引擎也不会影响其他监听器。这种隔离方式与 `subagent/start`/`subagent/end` 一致。 +`workflow/*` 事件(`workflow/start`、`workflow/phase`、`workflow/log`、`workflow/agent-start`、`workflow/agent-end`、`workflow/end`,见[事件目录](#cordis-surface))是**仅供观察**的 emit,携带数据快照:每个 payload 以 `WorkflowRunInfo`(id + meta)开头,而非活跃的 `WorkflowRun`,因此订阅者无法获得 `cancel`/`dispose`;`workflow/end` 刻意省略 result value(观察结果的监听器不得收到调用方 result 的可变别名)。每次 emit 对每个监听器隔离:抛出异常的订阅者被记录日志但不传播,不会饿死在它之后注册的监听器;每个监听器收到自己的 payload 克隆,因此修改它既不会损坏引擎也不会影响其他监听器。这种隔离方式与 `subagent/start`/`subagent/end` 一致。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.workflows` — `WorkflowService` (abstract seam) + +Workflow execution seam. Invalid requests throw before publication; a live run is holder-owned, its result never rejects, cancellation and disposal are bounded, and disposal waits for child cleanup within that bound. Lifecycle listener failures are contained, and `workflow/end` fires exactly once as the result settles. + +```ts cordis-catalog +/** + * Parse and execute a workflow script. + * @param request - the script, its `args`, the parent agent, and an + * optional cancel signal. + * @returns the live run; its `result` resolves when the script settles. + */ +abstract start(request: WorkflowStartRequest): WorkflowRun +``` + +Source: [`packages/workflow/workflow/src/index.ts:159`](../../packages/workflow/workflow/src/index.ts) + + + +### `workflow/*` events + + + +#### `workflow/agent-end` — emit + +One `agent()` call settled (clean result, child failure, or run cancellation). Paired with Events['workflow/agent-start'] by `agent.seq`, exactly once per started call on every stop path — on an engine termination path (a worker killed past its grace) the end is engine-synthesized with outcome `'cancelled'`. + +```ts cordis-catalog +/** + * One `agent()` call settled (clean result, child failure, or run + * cancellation). Paired with {@link Events['workflow/agent-start']} by + * `agent.seq`, exactly once per started call on every stop path — on an + * engine termination path (a worker killed past its grace) the end is + * engine-synthesized with outcome `'cancelled'`. + * @param info - the run's identity snapshot. + * @param agent - the call identity plus its outcome. + * @mode emit + */ +'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:81`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/agent-start` — emit + +One `agent()` call established a published child run. Paired with Events['workflow/agent-end'] by `agent.seq`. A call that never receives a published run from the provider emits neither event in this pair. + +```ts cordis-catalog +/** + * One `agent()` call established a published child run. Paired with + * {@link Events['workflow/agent-end']} by `agent.seq`. A call that never + * receives a published run from the provider emits neither + * event in this pair. + * @param info - the run's identity snapshot. + * @param agent - the call's sequence number, label, phase, and child id. + * @mode emit + */ +'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:70`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/end` — emit + +A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves. Paired with Events['workflow/start']. + +```ts cordis-catalog +/** + * A workflow run settled (any stop reason). Fired when + * {@link WorkflowRun.result} resolves. Paired with + * {@link Events['workflow/start']}. + * @param info - the run's identity snapshot. + * @param result - the outcome data (stop reason, error, agent count) — + * deliberately WITHOUT the result value (see {@link WorkflowResultInfo}). + * @mode emit + */ +'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:91`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/log` — emit + +The script emitted a narration line (a `log(message)` call). + +```ts cordis-catalog +/** + * The script emitted a narration line (a `log(message)` call). + * @param info - the run's identity snapshot. + * @param message - the logged message, verbatim. + * @mode emit + */ +'workflow/log'(info: WorkflowRunInfo, message: string): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:60`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/phase` — emit + +The script entered a phase (a `phase(title)` call) — progress grouping for observers; no execution semantics. + +```ts cordis-catalog +/** + * The script entered a phase (a `phase(title)` call) — progress grouping + * for observers; no execution semantics. + * @param info - the run's identity snapshot. + * @param title - the phase title, verbatim. + * @mode emit + */ +'workflow/phase'(info: WorkflowRunInfo, title: string): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:53`](../../packages/workflow/workflow/src/index.ts) + + + +#### `workflow/start` — emit + +A workflow run started — the script's meta block validated, the body about to execute. Paired with Events['workflow/end']. + +```ts cordis-catalog +/** + * A workflow run started — the script's meta block validated, the body + * about to execute. Paired with {@link Events['workflow/end']}. + * @param info - the run's identity snapshot (id + meta). + * @mode emit + */ +'workflow/start'(info: WorkflowRunInfo): void +``` + +Source: [`packages/workflow/workflow/src/index.ts:45`](../../packages/workflow/workflow/src/index.ts) + diff --git a/docs/subsystems/workspace.i18n.yaml b/docs/subsystems/workspace.i18n.yaml index 6a6488206c..b2c2248876 100644 --- a/docs/subsystems/workspace.i18n.yaml +++ b/docs/subsystems/workspace.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/workspace.md -workspace.md: 5614631e828e51f3b515ae4af17451bd9eda1dbb -workspace.zh.md: 11e8f0207a8db8b7612d6672cfe744523490aef7 +workspace.md: ca088a2091a7f47a3d52992fec13fae44061a608 +workspace.zh.md: e414c759a043f934e1a8b5d89c7a3b6101bbb6f4 diff --git a/docs/subsystems/workspace.md b/docs/subsystems/workspace.md index 5614631e82..ca088a2091 100644 --- a/docs/subsystems/workspace.md +++ b/docs/subsystems/workspace.md @@ -117,10 +117,103 @@ Ownership truth is the record's ordered `sessionIds`, never derived from session ## The registry: `ctx.workspace` -`WorkspaceRegistry` ([signatures](../cordis-catalog/services.md#ctxworkspace--workspaceregistry)) owns registration and resolution. `create(path, title?)` canonicalizes the path, rejects a nonexistent path (the original `ENOENT`) or a non-directory, returns the existing entity unchanged when the canonical path is already owned, and otherwise creates a record with `title ?? basename(path)` prepended to the durable registry order — a new record cannot duplicate an existing display title (`WorkspaceNameConflictError`). `get(id)` and the ordered `list()` are synchronous cache reads; `resolveByPath(path)` applies the same realpath canon without creating. `delete(id)` removes only the registration, order entry, and session account — the directory, user files, live sessions, and persisted logs are never touched, so those sessions become Ungrouped ([decision](../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md)); unknown ids return `false`. Create and delete persist a pending-mutation marker before their two writes (record + order) can diverge; startup resolves exactly the marked mutation — by deleting the marked table row, which completes an interrupted delete and rolls back an interrupted create (the registration is re-creatable, so rollback is the safe direction) — and an unmarked order/table mismatch fails loud as corruption. +`WorkspaceRegistry` ([signatures](#ctxworkspace--workspaceregistry)) owns registration and resolution. `create(path, title?)` canonicalizes the path, rejects a nonexistent path (the original `ENOENT`) or a non-directory, returns the existing entity unchanged when the canonical path is already owned, and otherwise creates a record with `title ?? basename(path)` prepended to the durable registry order — a new record cannot duplicate an existing display title (`WorkspaceNameConflictError`). `get(id)` and the ordered `list()` are synchronous cache reads; `resolveByPath(path)` applies the same realpath canon without creating. `delete(id)` removes only the registration, order entry, and session account — the directory, user files, live sessions, and persisted logs are never touched, so those sessions become Ungrouped ([decision](../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md)); unknown ids return `false`. Create and delete persist a pending-mutation marker before their two writes (record + order) can diverge; startup resolves exactly the marked mutation — by deleting the marked table row, which completes an interrupted delete and rolls back an interrupted create (the registration is re-creatable, so rollback is the safe direction) — and an unmarked order/table mismatch fails loud as corruption. Sessions get their cwd at create time from whoever creates them, not from this registry — the API gateway resolves a new session's cwd from the chosen workspace's `path` (falling back to an explicit or default cwd), creates the session so the cwd lands in its immutable [`SessionHeader`](persistence.md#sessionheader--metadata-beside-the-log), then calls `attachSession`, which re-validates that stored header cwd against the workspace path. On the first successful start, the registry bootstraps history from persisted headers alone (`id`, `cwd`, `createdAt` — never event bodies), grouping sessions with a valid canonical cwd into per-directory workspaces, newest first; the initialized marker is written last so an interrupted bootstrap resumes safely. The bootstrap is one-time: cwd-less legacy sessions stay Ungrouped, and sessions created afterwards join a workspace only through `attachSession`. ## Consumers [dsh-host-apiproxy](../../packages/host/apiproxy) is the product consumer: it serves workspace CRUD to GUI clients over `ctx.workspace` and performs the create-session-then-attach flow above. [dsh-workspace-context](../../packages/context/workspace-context) is **not** a consumer despite the name: it discovers AGENTS.md-style instruction files under an agent's own cwd and never touches `ctx.workspace` — the shared word refers to the user's working directory, not to this registry's entities. + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.directoryPicker` — `DirectoryPicker` (abstract seam) + +Abstract directory-picking service. Subclass, implement `capability()`, and load the subclass as a plugin — it registers as `ctx.directoryPicker` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior). The capability object must be stable for the service lifetime: consumers may capture it across calls. + +```ts cordis-catalog +/** + * The backend's interaction capability. + * @returns the discriminated capability consumers switch on. + */ +abstract capability(): DirectoryPickerCapability +``` + +Source: [`packages/host/directory-picker/src/index.ts:131`](../../packages/host/directory-picker/src/index.ts) + + + +### `ctx.workspace` — `WorkspaceRegistry` + +Durable workspace registry. Startup waits for `sessionPersistence`, builds one canonical-cwd header index, and completes the one-time history bootstrap before the service becomes active. The persistence dependency is mandatory so an unavailable peer can never be mistaken for an empty history and commit the initialized marker. + +```ts cordis-catalog +/** + * Create or reuse a workspace for an existing directory. The path is + * canonicalized through `fs.realpath`; a nonexistent path rejects with the + * original error and a non-directory rejects. Repeated calls for the same + * canonical path return the existing entity without changing its title. + * A newly created workspace is prepended to the durable registry order. + * Different canonical paths may share a display title. + * @param path - Existing directory to own, in any path spelling. + * @param title - Display title used only when a new record is created. + * @returns the existing or newly durable workspace. + */ +async create(path: string, title?: string): Promise + +/** + * Look up a workspace by id. + * @param id - Workspace id. + * @returns the workspace, or `undefined` when unknown. + */ +get(id: WorkspaceId): Workspace | undefined + +/** + * Synchronous workspace projection in durable registry order. Every + * entity's `sessionIds` getter is already filtered by the startup/live + * canonical-cwd header index; this method performs no persistence reads. + * @returns a fresh ordered array of workspace entities. + */ +list(): Workspace[] + +/** + * Delete one workspace registration while retaining its directory and every + * session log. The durable order is updated before the table deletion; a + * failed table write restores the prior order and keeps the entity + * published. Unknown ids are an idempotent no-op for domain callers. + * @param id - Workspace registration to remove. + * @returns `true` when a record was deleted, `false` when it was unknown. + */ +delete(id: WorkspaceId): Promise + +/** + * Archive one session durably. The session must exist (live or in session + * persistence); its workspace accounting — or lack of one — is irrelevant. + * An already archived id resolves without writing. + * @param sessionId - The session to archive. + * @returns resolution after durability. + */ +archiveSession(sessionId: SessionId): Promise + +/** + * Resolve by canonical directory path without creating or mutating a + * workspace. A missing path rejects during `realpath`; an existing unowned + * directory returns `undefined`. + * @param path - Existing directory path in any spelling. + * @returns the workspace owning the canonical path, when one exists. + */ +async resolveByPath(path: string): Promise +``` + +Types: [SessionId](core.md) + +Source: [`packages/workspace/workspace/src/index.ts:81`](../../packages/workspace/workspace/src/index.ts) + diff --git a/docs/subsystems/workspace.zh.md b/docs/subsystems/workspace.zh.md index 11e8f0207a..e414c759a0 100644 --- a/docs/subsystems/workspace.zh.md +++ b/docs/subsystems/workspace.zh.md @@ -117,10 +117,103 @@ interface Workspace { ## 注册表:`ctx.workspace` -`WorkspaceRegistry`([签名](../cordis-catalog/services.md#ctxworkspace--workspaceregistry))拥有注册与解析。`create(path, title?)` 规范化路径,拒绝不存在的路径(原样传出原始 `ENOENT`)或非目录;当规范路径已被拥有时原样返回既有实体;否则创建一条标题为 `title ?? basename(path)` 的记录并前插到持久的注册表顺序中——新记录不得与既有显示标题重复(`WorkspaceNameConflictError`)。`get(id)` 与有序的 `list()` 是同步缓存读取;`resolveByPath(path)` 应用同一套 realpath 规范但不创建。`delete(id)` 只移除注册记录、顺序条目和会话账本——目录、用户文件、实时会话和已持久化日志一概不动,因此这些会话变为 Ungrouped([决策](../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md));未知 id 返回 `false`。create 与 delete 会在其两次写入(记录 + 顺序)可能分叉之前先持久写入一个待定变更标记;启动时恰好解决被标记的那次变更——通过删除被标记的表行:这会补完被中断的 delete,并回滚被中断的 create(注册可以重建,因此回滚是安全方向)——而没有标记的顺序/表不一致则作为损坏大声失败。 +`WorkspaceRegistry`([签名](#ctxworkspace--workspaceregistry))拥有注册与解析。`create(path, title?)` 规范化路径,拒绝不存在的路径(原样传出原始 `ENOENT`)或非目录;当规范路径已被拥有时原样返回既有实体;否则创建一条标题为 `title ?? basename(path)` 的记录并前插到持久的注册表顺序中——新记录不得与既有显示标题重复(`WorkspaceNameConflictError`)。`get(id)` 与有序的 `list()` 是同步缓存读取;`resolveByPath(path)` 应用同一套 realpath 规范但不创建。`delete(id)` 只移除注册记录、顺序条目和会话账本——目录、用户文件、实时会话和已持久化日志一概不动,因此这些会话变为 Ungrouped([决策](../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md));未知 id 返回 `false`。create 与 delete 会在其两次写入(记录 + 顺序)可能分叉之前先持久写入一个待定变更标记;启动时恰好解决被标记的那次变更——通过删除被标记的表行:这会补完被中断的 delete,并回滚被中断的 create(注册可以重建,因此回滚是安全方向)——而没有标记的顺序/表不一致则作为损坏大声失败。 会话的 cwd 在创建时由创建者赋予,而不是由本注册表赋予——API 网关从所选工作区的 `path` 解析新会话的 cwd(回退到显式或默认 cwd),先创建会话使 cwd 落入其不可变的 [`SessionHeader`](persistence.md#sessionheader--metadata-beside-the-log),再调用 `attachSession`,后者会把已存储的 header cwd 与工作区路径重新校验一遍。首次成功启动时,注册表仅凭已持久化的 header(`id`、`cwd`、`createdAt`——绝不读事件正文)引导历史:把规范 cwd 有效的会话按目录分组为工作区,最新的排在最前;「已初始化」标记最后写入,因此被中断的引导可以安全续跑。引导只发生这一次:没有 cwd 的历史遗留会话保持 Ungrouped,此后创建的会话只能通过 `attachSession` 加入工作区。 ## 消费方 [dsh-host-apiproxy](../../packages/host/apiproxy) 是产品消费方:它经 `ctx.workspace` 向 GUI 客户端提供工作区的 CRUD,并执行上文「先建会话再 attach」的流程。[dsh-workspace-context](../../packages/context/workspace-context) 尽管名字如此,却**不是**消费方:它在 agent 自己的 cwd 下发现 AGENTS.md 风格的指令文件,从不触碰 `ctx.workspace`——两者共用的这个词指的是用户的工作目录,而非本注册表的实体。 + + + + + +## Cordis surface + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.directoryPicker` — `DirectoryPicker` (abstract seam) + +Abstract directory-picking service. Subclass, implement `capability()`, and load the subclass as a plugin — it registers as `ctx.directoryPicker` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior). The capability object must be stable for the service lifetime: consumers may capture it across calls. + +```ts cordis-catalog +/** + * The backend's interaction capability. + * @returns the discriminated capability consumers switch on. + */ +abstract capability(): DirectoryPickerCapability +``` + +Source: [`packages/host/directory-picker/src/index.ts:131`](../../packages/host/directory-picker/src/index.ts) + + + +### `ctx.workspace` — `WorkspaceRegistry` + +Durable workspace registry. Startup waits for `sessionPersistence`, builds one canonical-cwd header index, and completes the one-time history bootstrap before the service becomes active. The persistence dependency is mandatory so an unavailable peer can never be mistaken for an empty history and commit the initialized marker. + +```ts cordis-catalog +/** + * Create or reuse a workspace for an existing directory. The path is + * canonicalized through `fs.realpath`; a nonexistent path rejects with the + * original error and a non-directory rejects. Repeated calls for the same + * canonical path return the existing entity without changing its title. + * A newly created workspace is prepended to the durable registry order. + * Different canonical paths may share a display title. + * @param path - Existing directory to own, in any path spelling. + * @param title - Display title used only when a new record is created. + * @returns the existing or newly durable workspace. + */ +async create(path: string, title?: string): Promise + +/** + * Look up a workspace by id. + * @param id - Workspace id. + * @returns the workspace, or `undefined` when unknown. + */ +get(id: WorkspaceId): Workspace | undefined + +/** + * Synchronous workspace projection in durable registry order. Every + * entity's `sessionIds` getter is already filtered by the startup/live + * canonical-cwd header index; this method performs no persistence reads. + * @returns a fresh ordered array of workspace entities. + */ +list(): Workspace[] + +/** + * Delete one workspace registration while retaining its directory and every + * session log. The durable order is updated before the table deletion; a + * failed table write restores the prior order and keeps the entity + * published. Unknown ids are an idempotent no-op for domain callers. + * @param id - Workspace registration to remove. + * @returns `true` when a record was deleted, `false` when it was unknown. + */ +delete(id: WorkspaceId): Promise + +/** + * Archive one session durably. The session must exist (live or in session + * persistence); its workspace accounting — or lack of one — is irrelevant. + * An already archived id resolves without writing. + * @param sessionId - The session to archive. + * @returns resolution after durability. + */ +archiveSession(sessionId: SessionId): Promise + +/** + * Resolve by canonical directory path without creating or mutating a + * workspace. A missing path rejects during `realpath`; an existing unowned + * directory returns `undefined`. + * @param path - Existing directory path in any spelling. + * @returns the workspace owning the canonical path, when one exists. + */ +async resolveByPath(path: string): Promise +``` + +Types: [SessionId](core.md) + +Source: [`packages/workspace/workspace/src/index.ts:81`](../../packages/workspace/workspace/src/index.ts) + diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index a100469102..8986847d1e 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -3,7 +3,7 @@ # Tool Schema Catalog -Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the cordis [events](cordis-catalog/events.md) & [services](cordis-catalog/services.md) catalogs (the wiring a plugin listens to and calls) and [subsystems/](subsystems/core.md) (the types those signatures move) — this page is the *tools* the agent is offered. +Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the [subsystem pages](subsystems/core.md) (the types plus each page's generated `cordis-surface` wiring region) — this page is the *tools* the agent is offered. This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog Agent Note](../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md). diff --git a/docs/user/develop/framework/events.i18n.yaml b/docs/user/develop/framework/events.i18n.yaml index 769556da41..37dd601646 100644 --- a/docs/user/develop/framework/events.i18n.yaml +++ b/docs/user/develop/framework/events.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/user/develop/framework/events.md -events.md: fcbdb5f39bf2078032affbc6469f7eecc795d3ba -events.zh.md: 1979e0bc1dbb71e46f50a051dacee8ae3a1172a7 +events.md: c70150b4d9070063513021e420f253ff6d4e5de1 +events.zh.md: 813dc3631e0c776ad9c8763db65eebc3f2167578 diff --git a/docs/user/develop/framework/events.md b/docs/user/develop/framework/events.md index fcbdb5f39b..c70150b4d9 100644 --- a/docs/user/develop/framework/events.md +++ b/docs/user/develop/framework/events.md @@ -101,7 +101,7 @@ declare module 'cordis' { ## Cordis events and session records -Harness Cordis events use `namespace/action` names, including `agent/pre-step`, `agent/request`, `agent/request-error`, `tools/result`, and `session/event`. The generated [event catalog](../../../cordis-catalog/events.md) records complete signatures and modes. +Harness Cordis events use `namespace/action` names, including `agent/step`, `agent/request`, `agent/request-error`, `tools/result`, and `session/event`. The generated `cordis-surface` regions on the [subsystem pages](../../../subsystems/core.md) record complete signatures and modes. `turn/*`, `step/*`, `tool/call`, `tool/result`, and `compact/*` are durable session-event types, not same-named Cordis events. To observe them, listen to `session/event` and inspect `event.type`. diff --git a/docs/user/develop/framework/events.zh.md b/docs/user/develop/framework/events.zh.md index 1979e0bc1d..813dc3631e 100644 --- a/docs/user/develop/framework/events.zh.md +++ b/docs/user/develop/framework/events.zh.md @@ -101,7 +101,7 @@ declare module 'cordis' { ## Cordis 事件与会话记录 -Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request`、`agent/request-error`、`tools/result` 和 `session/event`。完整签名与触发模式见[Events 目录](../../../cordis-catalog/events.md)。 +Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/step`、`agent/request`、`agent/request-error`、`tools/result` 和 `session/event`。完整签名与触发模式见[子系统页面](../../../subsystems/core.md)上生成的 `cordis-surface` 区块。 `turn/*`、`step/*`、`tool/call`、`tool/result` 和 `compact/*` 是持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 `session/event` 并检查 `event.type`。 diff --git a/docs/user/develop/framework/service.i18n.yaml b/docs/user/develop/framework/service.i18n.yaml index ade6b8dc08..7cb2f4ff88 100644 --- a/docs/user/develop/framework/service.i18n.yaml +++ b/docs/user/develop/framework/service.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/user/develop/framework/service.md -service.md: 1bf28cb3c7dfdfbd6d0babfa3b1688ac65eea01e -service.zh.md: 78b9887a734b235bcff1d94a623e37cfeb2f41e3 +service.md: 040b1388cc431c30045e05f7d372ab5885bb3f9d +service.zh.md: 0786b684c1688440a24cc729288835ad636f8ff7 diff --git a/docs/user/develop/framework/service.md b/docs/user/develop/framework/service.md index 1bf28cb3c7..040b1388cc 100644 --- a/docs/user/develop/framework/service.md +++ b/docs/user/develop/framework/service.md @@ -140,7 +140,7 @@ This prevents a plugin from calling a service that no longer exists. ## Built-in Harness services -The repository generates the service names, public methods, and source locations in the [service catalog](../../../cordis-catalog/services.md). Use that catalog and the service's TypeScript interface while developing a plugin; do not maintain a second static list. +The repository generates the service names, public methods, and source locations into each service's [subsystem page](../../../subsystems/core.md). Use those generated regions and the service's TypeScript interface while developing a plugin; do not maintain a second static list. ## Next steps diff --git a/docs/user/develop/framework/service.zh.md b/docs/user/develop/framework/service.zh.md index 78b9887a73..0786b684c1 100644 --- a/docs/user/develop/framework/service.zh.md +++ b/docs/user/develop/framework/service.zh.md @@ -140,7 +140,7 @@ export function apply(ctx: Context) { ## Harness 内置服务 -仓库会自动生成[服务目录](../../../cordis-catalog/services.md),其中包含服务名、公开方法和源码位置。开发插件时应以该目录和服务的 TypeScript 接口为准,不要维护另一份静态清单。 +服务名、公开方法和源码位置由仓库自动生成到各服务的[子系统页面](../../../subsystems/core.md)。开发插件时应以这些生成区块和服务接口的 TypeScript 类型为准,不要复制一份静态清单。 ## 下一步 diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 2f165e76e4..fb18353290 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/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/core/agent-loop/README.md -README.md: 2fdc60086bebc924089b5b8bd12f4b4456b1ead5 -README.zh.md: df0ba44ec5b010666737c0568bc5eeb220ff0d96 +README.md: 81bc4411977f7335defef5af82d4a67a740b2ed5 +README.zh.md: c190171513e17777ebaadda0d279487876a412b4 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 2fdc60086b..81bc441197 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -74,7 +74,7 @@ Within a step, exclusive calls form barriers; parallel-safe calls use a bounded ### What belongs to plugins Everything that goes beyond "call the model, run the tools, repeat" belongs to plugins listening on the event taxonomy: -- Hooks and policy: the relevant `agent/*` checkpoints plus the guarded `tools/pre-execute` → `tools/execute` → `tools/post-execute` → definition-owned `finalizeContent` → `tools/result` pipeline; exact event signatures and modes live in the [generated event catalog](../../../docs/cordis-catalog/events.md) +- Hooks and policy: the relevant `agent/*` checkpoints plus the guarded `tools/pre-execute` → `tools/execute` → `tools/post-execute` → definition-owned `finalizeContent` → `tools/result` pipeline; exact event signatures and modes live in the generated regions of [core.md](../../../docs/subsystems/core.md#cordis-surface) and [tools.md](../../../docs/subsystems/tools.md#cordis-surface) - Compaction: pressure on `agent/pre-step`; canonical overflow repair on `agent/request-error` - Model-request recovery: `dsh-llm-retry` records and waits exact-provider normal or unbounded backoff on `agent/request-error`, emits non-surface `llm/retry` status, then returns a retry action - Sandbox, permission, plan mode: `tools/pre-execute` for extensible deny/ask, `tools.guard()` for monotonic owner policy, `tools/post-execute` for result decisions, and `tools/result` for final observation diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index df0ba44ec5..c190171513 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -74,7 +74,7 @@ interface Config { ### 插件负责的内容 超出「调用模型、运行工具、重复」的所有内容,都属于监听事件分类体系的插件: -- 钩子与策略:相关的 `agent/*` 检查点,加上受守卫保护的 `tools/pre-execute` → `tools/execute` → `tools/post-execute` → 定义拥有的 `finalizeContent` → `tools/result` 流水线;确切事件签名与 mode 位于生成的[事件目录](../../../docs/cordis-catalog/events.md) +- 钩子与策略:相关的 `agent/*` 检查点,加上受守卫保护的 `tools/pre-execute` → `tools/execute` → `tools/post-execute` → 定义拥有的 `finalizeContent` → `tools/result` 流水线;确切事件签名与 mode 位于 [core.md](../../../docs/subsystems/core.md#cordis-surface) 与 [tools.md](../../../docs/subsystems/tools.md#cordis-surface) 的生成区块 - 压缩(compaction):在 `agent/pre-step` 上观测压力;在 `agent/request-error` 上进行规范的溢出修复 - 模型请求恢复:`dsh-llm-retry` 在 `agent/request-error` 上记录并等待按确切提供方配置的 normal 或无界退避,发出不进入表层的 `llm/retry` 状态,然后返回重试动作 - 沙箱、权限、计划模式:使用 `tools/pre-execute` 提供可扩展的拒绝/询问,使用 `tools.guard()` 提供单调拥有方策略,使用 `tools/post-execute` 处理结果决定,并使用 `tools/result` 进行最终观测 diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index 5c03669baf..4c6009d3b0 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/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/core/agent/README.md -README.md: 2a69ab380eaad3929e27039582807037969eba64 -README.zh.md: 176f3f75cf0f6e3309b2f5d34afb4d562105608e +README.md: 3a3bdf6a4b3bdc5bfef250495e84b7d90b003822 +README.zh.md: 8164829e18ddd89fd84c0c2b5e033fd69241b9aa diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 2a69ab380e..3a3bdf6a4b 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -46,7 +46,7 @@ Agent *creation* is provided by the plugin implementing `AgentFactory` (`dsh-age ### Live events -`dsh-agent` declares the live `agent/*` coordination vocabulary so plugins do not depend on the concrete loop. Exact signatures, dispatch modes, scope-filtering rules, and payload contracts live in the generated [Cordis event catalog](../../../docs/cordis-catalog/events.md); the [architecture turn flow](../../../docs/architecture.md#turn-flow) shows their order relative to durable session events. +`dsh-agent` declares the live `agent/*` coordination vocabulary so plugins do not depend on the concrete loop. Exact signatures, dispatch modes, scope-filtering rules, and payload contracts live in the generated region of [core.md](../../../docs/subsystems/core.md#cordis-surface); the [architecture turn flow](../../../docs/architecture.md#turn-flow) shows their order relative to durable session events. The lifecycle edges have two important local caveats. `agent/created` runs after scoped setup and after both session and agent registry entries exist. Setup is trusted composition-only code; the immediately following non-vetoing `agent/session-start` notification is the first supported startup injection point. `agent/disposed` always means the exact agent has left the registry. AgentLoop emits it after its driver is quiescent, while ordered teardown may still be detaching the session and unwinding the scope; custom agents registered directly own any stronger driver-ordering contract themselves. diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index 176f3f75cf..8164829e18 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -46,7 +46,7 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供, ### 实时事件 -`dsh-agent` 声明实时 `agent/*` 协调词汇,使插件不必依赖具体循环。确切签名、分发 mode、作用域筛选规则与 payload 契约位于生成的 [Cordis 事件目录](../../../docs/cordis-catalog/events.md);[架构轮次流](../../../docs/architecture.md#turn-flow) 展示它们与持久会话事件的相对顺序。 +`dsh-agent` 声明实时 `agent/*` 协调词汇,使插件不必依赖具体循环。确切签名、分发 mode、作用域筛选规则与 payload 契约位于 [core.md](../../../docs/subsystems/core.md#cordis-surface) 的生成区块;[架构轮次流](../../../docs/architecture.md#turn-flow) 展示它们与持久会话事件的相对顺序。 生命周期边有两个重要的本地注意事项。`agent/created` 在作用域 setup 之后、会话与 agent 注册表条目都存在之后运行。Setup 是受信任、仅用于组合的代码;紧随其后且不可 veto 的 `agent/session-start` 通知是第一个受支持的启动注入点。`agent/disposed` 始终表示确切 agent 已离开注册表。AgentLoop 在其驱动器完全停稳后发出该事件,而有序 teardown 此时可能仍在分离会话并撤销作用域;直接注册的自定义 agent 自行拥有任何更强的驱动器顺序契约。 diff --git a/packages/core/session/README.i18n.yaml b/packages/core/session/README.i18n.yaml index c30b37e549..8b1e462166 100644 --- a/packages/core/session/README.i18n.yaml +++ b/packages/core/session/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/core/session/README.md -README.md: 07ee53006b390e5ad3979502b2f1aa24f5a6ce7a -README.zh.md: d4a90de10b92a489f8f575b926d758b45d8f5051 +README.md: 00e007ee6064f485bf80dfda0227ab6e834f6414 +README.zh.md: 5f06fee5d50bc2c9dc333874a8354fbf19d5248b diff --git a/packages/core/session/README.md b/packages/core/session/README.md index 07ee53006b..00e007ee60 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -30,7 +30,7 @@ Use the split lifecycle only when teardown must be ordered with another resource ### Live service events -The store pairs announced creation with disposal, publishes post-commit append notifications with per-listener containment, and provides an awaited durability checkpoint. Exact signatures and scope behavior live in the generated [event catalog](../../../docs/cordis-catalog/events.md); payloads live in the [persistence catalog](../../../docs/persistence-catalog.md). +The store pairs announced creation with disposal, publishes post-commit append notifications with per-listener containment, and provides an awaited durability checkpoint. Exact signatures and scope behavior live in the generated region of [session.md](../../../docs/subsystems/session.md#cordis-surface); payloads live in the [persistence catalog](../../../docs/persistence-catalog.md). ### Class: `Session` diff --git a/packages/core/session/README.zh.md b/packages/core/session/README.zh.md index d4a90de10b..5f06fee5d5 100644 --- a/packages/core/session/README.zh.md +++ b/packages/core/session/README.zh.md @@ -30,7 +30,7 @@ ### 实时服务事件 -会话存储会将已通知的创建与释放配对,在提交后发布追加通知并逐个监听器收容失败,同时提供受等待的持久性检查点。确切签名和作用域行为见生成的[事件目录](../../../docs/cordis-catalog/events.md);载荷见[持久化目录](../../../docs/persistence-catalog.md)。 +会话存储会将已通知的创建与释放配对,在提交后发布追加通知并逐个监听器收容失败,同时提供受等待的持久性检查点。确切签名和作用域行为见 [session.md](../../../docs/subsystems/session.md#cordis-surface) 的生成区块;载荷见[持久化目录](../../../docs/persistence-catalog.md)。 ### 类:`Session` diff --git a/packages/core/system-prompt/README.i18n.yaml b/packages/core/system-prompt/README.i18n.yaml index 36f166dd86..c94d1722dc 100644 --- a/packages/core/system-prompt/README.i18n.yaml +++ b/packages/core/system-prompt/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/core/system-prompt/README.md -README.md: 23bc0e8177ad2a778df9522e254bfd5e03a9871f -README.zh.md: b442239a50d539a8f079aa692c433d9defe57295 +README.md: e98c45a8829a945500ef5282d84e1904b31c68bf +README.zh.md: 5b9e2feaf82866a52cd8197ff5e800decdf3ee7e diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index 23bc0e8177..e98c45a882 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -23,7 +23,7 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem ### Live events -`system-prompt/assemble` is authoritative; listeners that replace entries must preserve any active Code Mode or structured-output protocol. Use [`ToolRegistry.restrict()`](../tools/README.md) when filtering must stay aligned across presentation, lookup, and execution. Registry-change notifications are unfiltered. The generated [event catalog](../../../docs/cordis-catalog/events.md) owns signatures and dispatch contracts. +`system-prompt/assemble` is authoritative; listeners that replace entries must preserve any active Code Mode or structured-output protocol. Use [`ToolRegistry.restrict()`](../tools/README.md) when filtering must stay aligned across presentation, lookup, and execution. Registry-change notifications are unfiltered. The generated region of [system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) owns signatures and dispatch contracts. ### Key types diff --git a/packages/core/system-prompt/README.zh.md b/packages/core/system-prompt/README.zh.md index b442239a50..5b9e2feaf8 100644 --- a/packages/core/system-prompt/README.zh.md +++ b/packages/core/system-prompt/README.zh.md @@ -23,7 +23,7 @@ ### 实时事件 -`system-prompt/assemble` 是权威来源;替换条目的监听器必须保留任何活动 Code Mode 或结构化输出协议。筛选需要在呈现、查找与执行之间保持一致时,应使用 [`ToolRegistry.restrict()`](../tools/README.md)。注册表变更通知不经过筛选。生成的[事件目录](../../../docs/cordis-catalog/events.md) 拥有签名与分发契约。 +`system-prompt/assemble` 是权威来源;替换条目的监听器必须保留任何活动 Code Mode 或结构化输出协议。筛选需要在呈现、查找与执行之间保持一致时,应使用 [`ToolRegistry.restrict()`](../tools/README.md)。注册表变更通知不经过筛选。[system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) 的生成区块拥有签名与分发契约。 ### 关键类型 diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml index e8157eed7e..b19e86ce84 100644 --- a/packages/core/tools/README.i18n.yaml +++ b/packages/core/tools/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/core/tools/README.md -README.md: fd5cb85540e42e62de28f96bf59890da9561f51e -README.zh.md: 5911b25b1d77710077063a643a29e500e4da8e95 +README.md: f3f25f908a4ba8016d38f7777fe72691dba4afb1 +README.zh.md: 796ee8e1aa8de88f979d825df5e0b4f39ab7bcf1 diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index fd5cb85540..f3f25f908a 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -35,7 +35,7 @@ Cancellation is cooperative and quiescent. Every typed invocation supplies a cal ### Live events -The live registry pipeline has three transformable waterfalls, then the definition-owned content finalizer, then the observe-only `tools/result` boundary; registry changes are deliberately unfiltered shared-state notifications. Exact signatures, dispatch modes, scope filtering, and failure-containment contracts live in the generated [Cordis event catalog](../../../docs/cordis-catalog/events.md), while the complete ordering is visualized in the generated [tool execution pipeline](../../../docs/tool-execution-pipeline.md). `tools/result` is live; the similarly named `tool/result` is the durable session event the agent loop appends afterwards. +The live registry pipeline has three transformable waterfalls, then the definition-owned content finalizer, then the observe-only `tools/result` boundary; registry changes are deliberately unfiltered shared-state notifications. Exact signatures, dispatch modes, scope filtering, and failure-containment contracts live in the generated region of [tools.md](../../../docs/subsystems/tools.md#cordis-surface), while the complete ordering is visualized in the generated [tool execution pipeline](../../../docs/tool-execution-pipeline.md). `tools/result` is live; the similarly named `tool/result` is the durable session event the agent loop appends afterwards. ### Key types @@ -56,7 +56,7 @@ The live registry pipeline has three transformable waterfalls, then the definiti - `tools/pre-execute` is the reorderable allow/deny/ask gate; `ctx.tools.guard()` adds monotonic owner policy after it. - `tools/execute` wraps normalized canonical dispatch for timeout, retry, or metrics. Wrappers may replace only the operational signal; a wrapper-authored success is normalized through the resolved tool's output declaration. Canonical-result provenance belongs to one immutable dispatch token, so a cached result from another call or tool is revalidated under the active declaration. - `tools/post-execute` may replace presentation content, replace the canonical value, block with feedback, or attach ordered contexts. A definition's optional `finalizeContent` then owns its last content-only invariant across normal results and outer pipeline failures; `tools/result` observes the immutable final outcome. Content replacement is not a confidentiality boundary: block or replace the value when programmatic consumers must not receive it. -- Exact signatures and ordering live in the generated [event catalog](../../../docs/cordis-catalog/events.md) and [pipeline](../../../docs/tool-execution-pipeline.md). +- Exact signatures and ordering live in the generated region of [tools.md](../../../docs/subsystems/tools.md#cordis-surface) and [pipeline](../../../docs/tool-execution-pipeline.md). - MCP servers: one plugin per server, discover tools, call `ctx.tools.register()` with the server's schemas. ### Typed tool parameter schemas diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md index 5911b25b1d..796ee8e1aa 100644 --- a/packages/core/tools/README.zh.md +++ b/packages/core/tools/README.zh.md @@ -35,7 +35,7 @@ tools: ### 实时事件 -实时注册表流水线先经过 3 个可变换的 waterfall,再经过由定义拥有的内容终结器,最后到达仅观测的 `tools/result` 边界;注册表变更有意作为不过滤的共享状态通知。确切签名、分发模式、作用域筛选和故障收容契约位于生成的 [Cordis 事件目录](../../../docs/cordis-catalog/events.md),完整顺序则在生成的[工具执行流水线](../../../docs/tool-execution-pipeline.md)中可视化。`tools/result` 是实时事件;名称相近的 `tool/result` 是 agent loop 随后追加的持久会话事件。 +实时注册表流水线先经过 3 个可变换的 waterfall,再经过由定义拥有的内容终结器,最后到达仅观测的 `tools/result` 边界;注册表变更有意作为不过滤的共享状态通知。确切签名、分发 mode、作用域筛选和故障收容契约位于 [tools.md](../../../docs/subsystems/tools.md#cordis-surface) 的生成区块,完整顺序则在生成的[工具执行流水线](../../../docs/tool-execution-pipeline.md)中可视化。`tools/result` 是实时事件;名称相近的 `tool/result` 是 agent loop 随后追加的持久会话事件。 ### 关键类型 @@ -56,7 +56,7 @@ tools: - `tools/pre-execute` 是可重排的允许/拒绝/询问门禁;`ctx.tools.guard()` 在其后添加单调的拥有方策略。 - `tools/execute` 会环绕包装规范化后的规范分发,以支持超时、重试或指标采集。包装层只能替换操作信号;包装层生成的成功结果会根据已解析工具的输出声明进行规范化。规范结果的来源属于一个不可变分发 token,因此,来自其他调用或工具的缓存结果会根据当前声明重新验证。 - `tools/post-execute` 可以替换呈现内容、替换规范值、通过反馈阻止,或附加有序上下文。随后,定义可选的 `finalizeContent` 会在普通结果和外层流水线失败中维护其最终、仅涉及内容的不变式;`tools/result` 观测不可变的最终结果。内容替换不是保密边界:当编程消费方不得接收某个值时,应阻止或替换该值。 -- 确切签名与顺序位于生成的[事件目录](../../../docs/cordis-catalog/events.md)和[流水线](../../../docs/tool-execution-pipeline.md)中。 +- 确切签名与顺序位于 [tools.md](../../../docs/subsystems/tools.md#cordis-surface) 的生成区块和[流水线](../../../docs/tool-execution-pipeline.md)中。 - MCP 服务器:每个服务器使用一个插件;发现工具后,使用服务器的 schema 调用 `ctx.tools.register()`。 ### 类型化工具参数 schema diff --git a/packages/fs/fs/README.i18n.yaml b/packages/fs/fs/README.i18n.yaml index d9b5f0ebc0..d9b0010670 100644 --- a/packages/fs/fs/README.i18n.yaml +++ b/packages/fs/fs/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/fs/fs/README.md -README.md: bf1dd1c1eb65146258cd64e450749845522e7057 -README.zh.md: f3fcc0c3794b972233dc418e93bdd80b1cc8570a +README.md: 5c58fc476b9b11a83bbe0d6c33ac782d94d6ffbc +README.zh.md: 97d7104f3c4c0beca408397f9cb1cead39b0a76e diff --git a/packages/fs/fs/README.md b/packages/fs/fs/README.md index bf1dd1c1eb..5c58fc476b 100644 --- a/packages/fs/fs/README.md +++ b/packages/fs/fs/README.md @@ -37,7 +37,7 @@ The mutation runs inside the backend's per-target lock either way, so an uncondi ## The `fs/*` policy events -This package declares three events (see the generated [events catalog](../../../docs/cordis-catalog/events.md)) so the emitter (`@deepseek-ai/dsh-tool-fs`) and the policy listener (`@deepseek-ai/dsh-fs-policy`) share a vocabulary without the emitter depending on the policy plugin. `fs/write-intent` and `fs/edit-intent` are single-slot decision waterfalls (the listener fully decides, never calling `next()`); `fs/observed` is a fire-and-forget recording event. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure. +This package declares three events (see the generated region of [filesystem.md](../../../docs/subsystems/filesystem.md#cordis-surface)) so the emitter (`@deepseek-ai/dsh-tool-fs`) and the policy listener (`@deepseek-ai/dsh-fs-policy`) share a vocabulary without the emitter depending on the policy plugin. `fs/write-intent` and `fs/edit-intent` are single-slot decision waterfalls (the listener fully decides, never calling `next()`); `fs/observed` is a fire-and-forget recording event. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure. ## A provider seam, not the policy layer diff --git a/packages/fs/fs/README.zh.md b/packages/fs/fs/README.zh.md index f3fcc0c379..97d7104f3c 100644 --- a/packages/fs/fs/README.zh.md +++ b/packages/fs/fs/README.zh.md @@ -37,7 +37,7 @@ ## `fs/*` 政策事件 -本包声明三个事件(见已生成的[事件目录](../../../docs/cordis-catalog/events.md)),使发出方(`@deepseek-ai/dsh-tool-fs`)和政策监听器(`@deepseek-ai/dsh-fs-policy`)共享词汇,而无需让发出方依赖政策插件。`fs/write-intent` 和 `fs/edit-intent` 是单槽决策 waterfall(监听器完整决策,绝不调用 `next()`);`fs/observed` 是发后即忘的记录事件。它们只携带 `dsh-fs` 词汇和一个不透明 `object` 参与者,不含面向模型的概念或 agent(智能体)/会话所有者结构。 +本包声明三个事件(见 [filesystem.md](../../../docs/subsystems/filesystem.md#cordis-surface) 的生成区块),使发出方(`@deepseek-ai/dsh-tool-fs`)和政策监听器(`@deepseek-ai/dsh-fs-policy`)共享词汇,而无需让发出方依赖政策插件。`fs/write-intent` 和 `fs/edit-intent` 是单槽决策 waterfall(监听器完整决策,绝不调用 `next()`);`fs/observed` 是发后即忘的记录事件。它们只携带 `dsh-fs` 词汇和一个不透明 `object` 参与者,不含面向模型的概念或 agent(智能体)/会话所有者结构。 ## 提供方 seam,不是政策层 diff --git a/packages/goal/goal/README.i18n.yaml b/packages/goal/goal/README.i18n.yaml index 6362a6d1e9..d8fbf8fc29 100644 --- a/packages/goal/goal/README.i18n.yaml +++ b/packages/goal/goal/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/goal/goal/README.md -README.md: fc2fd8791096bcc9077ef7972da86b0e07c6f18e -README.zh.md: 311ec1d03436cc45bbea28df96e66579852058b0 +README.md: 98765a809820bcd4aeba31e1dd4ef6e92eb30760 +README.zh.md: dbc499104bac6cf09f9b1c871143da8781a8f082 diff --git a/packages/goal/goal/README.md b/packages/goal/goal/README.md index fc2fd87910..98765a8098 100644 --- a/packages/goal/goal/README.md +++ b/packages/goal/goal/README.md @@ -17,7 +17,7 @@ Event-sourced same-session goal state. The service retains one current completio ## Service contract -`ctx.goals` accepts only the exact live `Agent` instance registered under its id. `get()` returns a detached `GoalView`; mutations use a `GoalRef { id, revision }` compare-and-set fence and reject stale refs. The service exposes create, edit, pause, resume, complete, block, and clear verbs through the generated [service catalog](../../../docs/cordis-catalog/services.md). Creation default resolution is internal. `disarm()` is the lifecycle-only exception: it removes process-local continuation authority without writing a revision or emitting a mutation. +`ctx.goals` accepts only the exact live `Agent` instance registered under its id. `get()` returns a detached `GoalView`; mutations use a `GoalRef { id, revision }` compare-and-set fence and reject stale refs. The service exposes create, edit, pause, resume, complete, block, and clear verbs through the generated region of [goal.md](../../../docs/subsystems/goal.md#cordis-surface). Creation default resolution is internal. `disarm()` is the lifecycle-only exception: it removes process-local continuation authority without writing a revision or emitting a mutation. At most one goal is current. Creation produces an active revision-one goal and arms it. A non-complete goal must be edited, transitioned, or cleared; a completed goal may be replaced by a globally fresh id. Edits retain phase, blocker reason, and activation. Pause, completion, blocking, and clear disarm activation. A block records a policy-owned lower-kebab-case code plus a normalized free-form explanation; provider limits, configured budgets, execution errors, and requests for human input all use this one durable phase rather than multiplying lifecycle states. Resume accepts a stopped phase or a disarmed active goal only while the configured round cap has remaining capacity; it clears any former blocker reason. An active armed goal rejects the redundant operation. diff --git a/packages/goal/goal/README.zh.md b/packages/goal/goal/README.zh.md index 311ec1d034..dbc499104b 100644 --- a/packages/goal/goal/README.zh.md +++ b/packages/goal/goal/README.zh.md @@ -17,7 +17,7 @@ ## 服务契约 -`ctx.goals` 只接受以对应 id 注册的完全相同的活跃 `Agent` 实例。`get()` 返回与内部状态脱离的 `GoalView`;变更通过 `GoalRef { id, revision }` 比较并设置屏障,并拒绝陈旧引用。服务通过生成的[服务目录](../../../docs/cordis-catalog/services.md)公开 create、edit、pause、resume、complete、block 和 clear 动词。创建默认值在内部解析。`disarm()` 是仅供生命周期使用的例外:它移除进程本地续行权限,不写入新 revision,也不发出变更。 +`ctx.goals` 只接受以对应 id 注册的完全相同的活跃 `Agent` 实例。`get()` 返回分离的 `GoalView`;变更通过 `GoalRef { id, revision }` 比较并设置限制,并拒绝陈旧引用。服务通过 [goal.md](../../../docs/subsystems/goal.md#cordis-surface) 的生成区块公开 create、edit、pause、resume、complete、block 和 clear 动词。创建默认值在内部解析。`disarm()` 是仅供生命周期使用的例外:它移除进程本地续行权限,不写入新 revision,也不发送变更事件。 最多只有一个当前目标。创建操作会生成 revision 为 1、phase 为 active 的目标并启用续行。未完成的目标必须编辑、转换或清除;已完成目标可以由拥有全局未使用过的 id 的目标替换。编辑会保留 phase、blocker reason 与 activation。暂停、完成、阻塞和清除都会停用续行。阻塞会记录策略自有的 lower-kebab-case 代码和规范化的自由文本说明;提供方限制、配置预算、执行错误与请求人工输入都使用这一种持久 phase,不会扩增生命周期状态。只有配置的 Round 上限仍有剩余容量时,resume 才接受已停止 phase 或 phase 为 active 但已停用续行的目标;它会清除原 blocker reason。phase 为 active 且已启用续行的目标会拒绝冗余操作。 diff --git a/packages/interaction/user-approval/README.i18n.yaml b/packages/interaction/user-approval/README.i18n.yaml index 36188334e7..2122c0d101 100644 --- a/packages/interaction/user-approval/README.i18n.yaml +++ b/packages/interaction/user-approval/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/user-approval/README.md -README.md: 7b87a75d1c7c43874c484bc11f8deed45cb523ce -README.zh.md: 3c02f8d673a62c3ee954da26f6341b433032a88c +README.md: 0cf5d458863194e29f8c84168a6f089baabbf3d2 +README.zh.md: a48e4ff4650fbd90744c9d7c41878b6e0487127c diff --git a/packages/interaction/user-approval/README.md b/packages/interaction/user-approval/README.md index 7b87a75d1c..0cf5d45886 100644 --- a/packages/interaction/user-approval/README.md +++ b/packages/interaction/user-approval/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Channel-neutral one-shot approval seam. `ctx.approval.request(req)` returns `allowed-once`, `rejected`, `cancelled`, or `unavailable`; missing or failing answerers fail closed, and a grant applies only to the requested action. Exact event signatures live in the generated [Cordis catalog](../../../docs/cordis-catalog/events.md). +Channel-neutral one-shot approval seam. `ctx.approval.request(req)` returns `allowed-once`, `rejected`, `cancelled`, or `unavailable`; missing or failing answerers fail closed, and a grant applies only to the requested action. Exact event signatures live in the generated region of [approval.md](../../../docs/subsystems/approval.md#cordis-surface). Each request must belong to an open agent turn. The service appends a paired `approval/asked` and `approval/decided` audit record, while the model sees only the resulting logged tool outcome. An aborted request resolves `cancelled`; an audit append that fails before commit rejects rather than returning an unlogged decision. diff --git a/packages/interaction/user-approval/README.zh.md b/packages/interaction/user-approval/README.zh.md index 3c02f8d673..a48e4ff465 100644 --- a/packages/interaction/user-approval/README.zh.md +++ b/packages/interaction/user-approval/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -与通道无关的一次性审批 seam。`ctx.approval.request(req)` 返回 `allowed-once`、`rejected`、`cancelled` 或 `unavailable`;应答者缺失或失败时会以拒绝方式关闭,授权也只适用于所请求的操作。确切事件签名见生成的 [Cordis 目录](../../../docs/cordis-catalog/events.md)。 +与通道无关的一次性审批 seam。`ctx.approval.request(req)` 返回 `allowed-once`、`rejected`、`cancelled` 或 `unavailable`;应答者缺失或失败时会以拒绝方式关闭,授权也只适用于所请求的操作。确切事件签名见 [approval.md](../../../docs/subsystems/approval.md#cordis-surface) 的生成区块。 每个请求都必须属于一个尚未结束的 agent(智能体)轮次。服务会追加一对 `approval/asked` 与 `approval/decided` 审计记录,而模型只会看到由此产生且已写入日志的工具结果。已中止的请求会解析为 `cancelled`;如果审计记录的追加在提交前失败,Promise 会被拒绝,而不会返回一项未记录的决定。 diff --git a/packages/self-modification/tool-cordis/README.i18n.yaml b/packages/self-modification/tool-cordis/README.i18n.yaml index c25dda28e3..ef1f441711 100644 --- a/packages/self-modification/tool-cordis/README.i18n.yaml +++ b/packages/self-modification/tool-cordis/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/self-modification/tool-cordis/README.md -README.md: 9986310160c2b56126155a4c3ef84d66018d31c2 -README.zh.md: d955306e1e5d4154f58c771704782ece44a15c99 +README.md: f2a65043a1d2f74553e98caf59ed3d38b5a70b7c +README.zh.md: 66742094992d219ccfbd60b935dcd10e48cb12b8 diff --git a/packages/self-modification/tool-cordis/README.md b/packages/self-modification/tool-cordis/README.md index 9986310160..f2a65043a1 100644 --- a/packages/self-modification/tool-cordis/README.md +++ b/packages/self-modification/tool-cordis/README.md @@ -28,7 +28,7 @@ The sandbox isolates globals but is not a security boundary. Node globals are ab ## The generated API catalog -`src/api-catalog.ts` is generated from the same Typert `FaceModel` projection as [docs/cordis-catalog](../../../docs/cordis-catalog/services.md) and freshness-gated by `pnpm run verify-cordis-api` (in `doc-sync`) — never edit it by hand. `scripts/gen-cordis-api.ts` is a compatibility entry point for that unified projection, not a second collector. `cordis_inspect` intersects the committed catalog with the live service store at call time; it has no runtime Typert dependency. Broad `api` / `events` reports render summaries and signatures only; an exact `name` opts into the retained method/event JSDoc, and unknown or non-running service targets fail loud. +`src/api-catalog.ts` is generated from the same Typert `FaceModel` projection as the [subsystem pages' generated regions](../../../docs/subsystems/core.md) and freshness-gated by `pnpm run verify-cordis-api` (in `doc-sync`) — never edit it by hand. `scripts/gen-cordis-api.ts` is a compatibility entry point for that unified projection, not a second collector. `cordis_inspect` intersects the committed catalog with the live service store at call time; it has no runtime Typert dependency. Broad `api` / `events` reports render summaries and signatures only; an exact `name` opts into the retained method/event JSDoc, and unknown or non-running service targets fail loud. ## Rendering diff --git a/packages/self-modification/tool-cordis/README.zh.md b/packages/self-modification/tool-cordis/README.zh.md index d955306e1e..6674209499 100644 --- a/packages/self-modification/tool-cordis/README.zh.md +++ b/packages/self-modification/tool-cordis/README.zh.md @@ -28,7 +28,7 @@ ## 生成的 API 目录 -`src/api-catalog.ts` 与 [docs/cordis-catalog](../../../docs/cordis-catalog/services.md) 由同一个 Typert `FaceModel` 投影生成,并由 `pnpm run verify-cordis-api`(位于 `doc-sync` 中)实施新鲜度门禁,绝不可手工编辑。`scripts/gen-cordis-api.ts` 是该统一投影的兼容入口,而非第二套收集器。`cordis_inspect` 在调用时把已提交的目录与存活服务 store 取交集;它在运行时不依赖 Typert。宽泛的 `api`/`events` 报告只渲染摘要与签名;精确 `name` 会选择保留的方法/事件 JSDoc,未知或未运行的服务目标会高声失败。 +`src/api-catalog.ts` 与[子系统页面的生成区块](../../../docs/subsystems/core.md)由同一个 Typert `FaceModel` 投影生成,并由 `pnpm run verify-cordis-api`(位于 `doc-sync` 中)实施新鲜度门禁,绝不可手工编辑。`scripts/gen-cordis-api.ts` 是该统一投影的兼容入口,而非第二套收集器。`cordis_inspect` 在调用时把已提交的目录与存活服务 store 取交集;它在运行时不依赖 Typert。宽泛的 `api`/`events` 报告只渲染摘要与签名;精确 `name` 会选择保留的方法/事件 JSDoc,未知或未运行的服务目标会高声失败。 ## 渲染 diff --git a/packages/typert/generator/src/cordis-catalog.ts b/packages/typert/generator/src/cordis-catalog.ts index d62739acdd..1950c255cb 100644 --- a/packages/typert/generator/src/cordis-catalog.ts +++ b/packages/typert/generator/src/cordis-catalog.ts @@ -694,32 +694,61 @@ function renderRuntimeApi( lines.push(']', '') return lines.join('\n') } -/** Render the cross-link "Types:" line for a signature, or '' if none apply. */ -function typeLinks(signature: string, linkedTypePages: Readonly>): string { +/** Opening region delimiter; injected content lives between the pair and the page owns everything outside. */ +export const REGION_BEGIN = '' +/** Closing region delimiter matching {@link REGION_BEGIN}. */ +export const REGION_END = '' + +/** + * Render the cross-link "Types:" line for a signature relative to one + * subsystems page, or '' if none apply. A type whose primary page IS the + * rendering page would link as a fragmentless self-link readers already sit + * on, so it is dropped instead. + */ +function typeLinks(signature: string, onPage: string, linkedTypePages: Readonly>): string { const seen = new Set() for (const name of Object.keys(linkedTypePages)) { if (new RegExp(`\\b${name}\\b`).test(signature)) seen.add(name) } - if (seen.size === 0) return '' - const links = [...seen].sort().map(n => `[${n}](../subsystems/${linkedTypePages[n]})`) + const links = [...seen].sort() + .filter(name => linkedTypePages[name] !== onPage) + .map(name => `[${name}](${linkedTypePages[name]})`) + if (links.length === 0) return '' return `Types: ${links.join(' · ')}` } -/** Render one harness event entry. */ -function renderEvent(e: EventEntry, linkedTypePages: Readonly>): string[] { - const out = [`### \`${e.name}\` — ${e.mode}`, ''] +/** + * GitHub's heading-slug algorithm (lowercase; drop everything but letters, + * numbers, spaces, hyphens; spaces become hyphens). Region headings carry + * backticks and em-dashes, which VitePress slugifies differently, so each + * generated heading is preceded by an explicit `` carrying this slug — + * the historical flat-catalog anchor — making `#ctx--` fragments + * resolve identically on GitHub and the published site. + */ +function githubSlug(heading: string): string { + return heading.toLowerCase().replace(/[^\p{L}\p{N} -]/gu, '').replaceAll(' ', '-') +} + +/** The explicit-anchor line emitted before one generated heading. */ +function anchorFor(headingText: string): string[] { + return [``, ''] +} + +/** Render one harness event entry onto its owning page, nested under its scope heading. */ +function renderEvent(e: EventEntry, onPage: string, linkedTypePages: Readonly>): string[] { + const out = [...anchorFor(`${e.name} — ${e.mode}`), `#### \`${e.name}\` — ${e.mode}`, ''] if (e.doc) out.push(e.doc, '') out.push('```' + FENCE, e.jsDoc, e.signature, '```', '') - const links = typeLinks(e.signature, linkedTypePages) + const links = typeLinks(e.signature, onPage, linkedTypePages) if (links) out.push(links, '') out.push(`Source: [\`${e.source}\`](../../${e.source.split(':')[0]})`, '') return out } -/** Render one harness service entry. */ -function renderService(s: ServiceEntry, linkedTypePages: Readonly>): string[] { +/** Render one harness service entry onto its owning page. */ +function renderService(s: ServiceEntry, onPage: string, linkedTypePages: Readonly>): string[] { const kind = s.abstract ? ' (abstract seam)' : '' - const out = [`## \`ctx.${s.key}\` — \`${s.type}\`${kind}`, ''] + const out = [...anchorFor(`ctx.${s.key} — ${s.type}${kind}`), `### \`ctx.${s.key}\` — \`${s.type}\`${kind}`, ''] if (s.doc) out.push(s.doc, '') if (s.methods.length) { const declarations = s.methods.flatMap((method, index) => [ @@ -728,7 +757,7 @@ function renderService(s: ServiceEntry, linkedTypePages: Readonly method.signature).join('\n'), linkedTypePages) + const links = typeLinks(s.methods.map(method => method.signature).join('\n'), onPage, linkedTypePages) if (links) out.push(links, '') } out.push(`Source: [\`${s.source}\`](../../${s.source.split(':')[0]})`, '') @@ -746,36 +775,62 @@ const BANNER = [ const GATE_NOTICE = 'This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.' /** - * Render the events catalog deterministically. - * @param events - validated event entries to render. - * @param policy - type links and inherited events supplied by the caller. + * Render one page's generated `cordis-surface` region: the services mapped to + * the page, then the event scopes mapped to it, markers included. Pure and + * deterministic given sorted inputs; identical bytes land in both pair sides. + * @param page - the owning `docs/subsystems/` page basename, e.g. `core.md`. + * @param services - validated services mapped to this page. + * @param events - validated events whose scopes map to this page. + * @param policy - type links supplied by the caller. + * @returns the complete marker-delimited region text. + */ +export function renderPageRegion(page: string, services: ServiceEntry[], events: EventEntry[], policy: CordisCatalogPolicy): string { + const lines: string[] = [ + REGION_BEGIN, + '', + '', + '', + '## Cordis surface', + '', + 'Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).', + '', + ] + for (const s of services) lines.push(...renderService(s, page, policy.linkedTypePages)) + const scopes = [...new Set(events.map(e => e.scope))].sort() + for (const scope of scopes) { + lines.push(...anchorFor(`${scope}/* events`), `### \`${scope}/*\` events`, '') + for (const e of events.filter(x => x.scope === scope).sort((a, b) => a.name.localeCompare(b.name))) { + lines.push(...renderEvent(e, page, policy.linkedTypePages)) + } + } + while (lines.at(-1) === '') lines.pop() + lines.push(REGION_END) + return lines.join('\n') +} + +/** + * Render the inherited (pinned vendor) tier as its own generated page. + * @param policy - inherited events and services supplied by the caller. * @returns the complete generated Markdown document. */ -export function renderEvents(events: EventEntry[], policy: CordisCatalogPolicy): string { +export function renderInheritedPage(policy: CordisCatalogPolicy): string { const lines: string[] = [ ...BANNER, - '# Cordis Events Catalog', + '# Inherited Cordis Surface', '', - 'Every cordis event a plugin can listen to: exact signature, dispatch mode, and original declaration JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [subsystems/](../subsystems/core.md) catalogs the *data structures* these signatures move around.', + 'The framework `ctx` members and events every plugin sees beyond the harness tier — pinned vendor source ([vendoring policy](../../vendor/README.md)), summarized tersely so the harness pages stay focused on repository-owned vocabulary. Detailed Context, Fiber, Registry, and Service APIs are generated in [context.md](context.md), [fiber.md](fiber.md), [registry.md](registry.md), and [service.md](service.md); the event-dispatch methods in [events.md](events.md).', '', GATE_NOTICE, '', - 'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).', - '', - 'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).', + '## Inherited `ctx` members (cordis core + loader/hmr/timer)', '', ] - const scopes = [...new Set(events.map(e => e.scope))].sort() - for (const scope of scopes) { - lines.push(`## \`${scope}/*\``, '') - for (const e of events.filter(x => x.scope === scope).sort((a, b) => a.name.localeCompare(b.name))) { - lines.push(...renderEvent(e, policy.linkedTypePages)) - } + for (const s of policy.inheritedServices) { + lines.push(`- \`${s.name}\` — ${s.summary} ([\`${s.source}\`](../../${s.source.split(':')[0]}))`) } lines.push( - '## Inherited events (cordis core + loader/hmr/timer)', '', - 'The framework events every plugin also sees, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of the event bus, without elevating framework internals to the harness tier\'s prominence.', + '## Inherited events (cordis core + loader/hmr/timer)', '', ) for (const e of policy.inheritedEvents) { @@ -784,35 +839,3 @@ export function renderEvents(events: EventEntry[], policy: CordisCatalogPolicy): lines.push('') return lines.join('\n') } - -/** - * Render the services catalog deterministically. - * @param services - validated service entries to render. - * @param policy - type links and inherited services supplied by the caller. - * @returns the complete generated Markdown document. - */ -export function renderServices(services: ServiceEntry[], policy: CordisCatalogPolicy): string { - const lines: string[] = [ - ...BANNER, - '# Cordis Services Catalog', - '', - 'Every `ctx.` service a plugin can call: the exact public interface with original method JSDoc, plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [subsystems/](../subsystems/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against.', - '', - GATE_NOTICE, - '', - 'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md).', - '', - ] - for (const s of services) lines.push(...renderService(s, policy.linkedTypePages)) - lines.push( - '## Inherited `ctx` members (cordis core + loader/hmr/timer)', - '', - 'The framework `ctx` surface every plugin also sees, beyond the harness services above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of what `ctx` offers, without elevating framework internals to the harness tier\'s prominence.', - '', - ) - for (const s of policy.inheritedServices) { - lines.push(`- \`${s.name}\` — ${s.summary} ([\`${s.source}\`](../../${s.source.split(':')[0]}))`) - } - lines.push('') - return lines.join('\n') -} diff --git a/packages/typert/generator/tests/cordis-catalog-contract.spec.ts b/packages/typert/generator/tests/cordis-catalog-contract.spec.ts index 1b6589b6cf..bde52505dc 100644 --- a/packages/typert/generator/tests/cordis-catalog-contract.spec.ts +++ b/packages/typert/generator/tests/cordis-catalog-contract.spec.ts @@ -10,8 +10,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { collectEvents as collectEventsWithPolicy, collectServices as collectServicesWithPolicy, - renderEvents as renderEventsWithPolicy, - renderServices as renderServicesWithPolicy, + renderPageRegion, } from '../src/cordis-catalog.ts' import type { CordisCatalogPolicy, @@ -35,12 +34,12 @@ function collectServices(root: string): ServiceEntry[] { return collectServicesWithPolicy(root, TEST_POLICY) } -function renderEvents(events: EventEntry[]): string { - return renderEventsWithPolicy(events, TEST_POLICY) +function renderEvents(events: EventEntry[], onPage = 'bash.md'): string { + return renderPageRegion(onPage, [], events, TEST_POLICY) } -function renderServices(services: ServiceEntry[]): string { - return renderServicesWithPolicy(services, TEST_POLICY) +function renderServices(services: ServiceEntry[], onPage = 'bash.md'): string { + return renderPageRegion(onPage, services, [], TEST_POLICY) } const TYPE_FIXTURES = [ @@ -155,7 +154,8 @@ describe.skip('gen-cordis-catalog collectEvents', { timeout: 60_000 }, () => { ' /**\n * Carry linked and foundation types.\n * @param value - the linked value.\n * @param preset - deployment metadata documented outside the subsystems catalog.\n * @param signal - cancellation.\n * @mode parallel\n */\n \'fix/typed\'(value: Readonly, preset: PresetSpec, signal: AbortSignal): Promise', )) expect(events).toHaveLength(1) - expect(renderEvents(events)).toContain('Types: [SessionEvent](../subsystems/core.md)') + expect(renderEvents(events)).toContain('Types: [SessionEvent](core.md)') + expect(renderEvents(events, 'core.md')).not.toContain('Types: [SessionEvent]') expect(renderEvents(events)).not.toContain('[PresetSpec]') }) diff --git a/packages/typert/generator/tests/cordis-catalog.spec.ts b/packages/typert/generator/tests/cordis-catalog.spec.ts index 644bb28de2..9474c15145 100644 --- a/packages/typert/generator/tests/cordis-catalog.spec.ts +++ b/packages/typert/generator/tests/cordis-catalog.spec.ts @@ -3,10 +3,10 @@ import { join, resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { projectCordisCatalog, - renderEvents, - renderServices, + renderInheritedPage, + renderPageRegion, } from '../src/cordis-catalog.ts' -import { CORDIS_CATALOG_POLICY } from '../../../../scripts/gen-cordis-catalog.ts' +import { CORDIS_CATALOG_POLICY, EVENT_SCOPE_PAGE, REGION_BEGIN, REGION_END, SERVICE_PAGE } from '../../../../scripts/gen-cordis-catalog.ts' const workspaceRoot = resolve(import.meta.dirname, '../../../..') @@ -15,8 +15,22 @@ describe('Typert-backed Cordis catalog', () => { const { projector, model } = projectCordisCatalog(workspaceRoot, CORDIS_CATALOG_POLICY) const expected = (path: string): string => readFileSync(join(workspaceRoot, path), 'utf8') - expect(renderEvents([...model.events], CORDIS_CATALOG_POLICY)).toBe(expected('docs/cordis-catalog/events.md')) - expect(renderServices([...model.services], CORDIS_CATALOG_POLICY)).toBe(expected('docs/cordis-catalog/services.md')) + expect(renderInheritedPage(CORDIS_CATALOG_POLICY)).toBe(expected('docs/cordis-api/inherited.md')) + for (const page of [...new Set([...Object.values(SERVICE_PAGE), ...Object.values(EVENT_SCOPE_PAGE)])].sort()) { + const region = renderPageRegion( + page, + [...model.services].filter(s => SERVICE_PAGE[s.key] === page), + [...model.events].filter(e => EVENT_SCOPE_PAGE[e.scope] === page), + CORDIS_CATALOG_POLICY, + ) + for (const side of [page, page.replace(/\.md$/, '.zh.md')]) { + const committed = expected(`docs/subsystems/${side}`) + const begin = committed.indexOf(REGION_BEGIN) + const end = committed.indexOf(REGION_END) + expect(begin, `docs/subsystems/${side} carries the region`).toBeGreaterThanOrEqual(0) + expect(committed.slice(begin, end + REGION_END.length)).toBe(region) + } + } expect(projector.renderRuntimeApi(model)).toBe( expected('packages/self-modification/tool-cordis/src/api-catalog.ts'), ) diff --git a/scripts/cordis-core-api.spec.ts b/scripts/cordis-core-api.spec.ts index d35899553c..8aef4041a1 100644 --- a/scripts/cordis-core-api.spec.ts +++ b/scripts/cordis-core-api.spec.ts @@ -21,13 +21,13 @@ describe('Cordis core API generation', () => { it('renders the five detailed pages from pinned vendor declarations', () => { const pages = renderCordisCoreApiPages() expect([...pages.keys()]).toEqual(CORDIS_CORE_API_PAGES.map(page => page.out)) - expect(pages.get('docs/cordis-catalog/core/context.md')).toContain('### ctx.extend(meta?)') - expect(pages.get('docs/cordis-catalog/core/events.md')).toContain('## DispatchMode') - expect(pages.get('docs/cordis-catalog/core/fiber.md')).toContain('## EffectMeta') - expect(pages.get('docs/cordis-catalog/core/registry.md')).toContain('## Plugin') - expect(pages.get('docs/cordis-catalog/core/service.md')).toContain('### Service.resolveConfig') + expect(pages.get('docs/cordis-api/context.md')).toContain('### ctx.extend(meta?)') + expect(pages.get('docs/cordis-api/events.md')).toContain('## DispatchMode') + expect(pages.get('docs/cordis-api/fiber.md')).toContain('## EffectMeta') + expect(pages.get('docs/cordis-api/registry.md')).toContain('## Plugin') + expect(pages.get('docs/cordis-api/service.md')).toContain('### Service.resolveConfig') - const fiber = pages.get('docs/cordis-catalog/core/fiber.md') ?? '' + const fiber = pages.get('docs/cordis-api/fiber.md') ?? '' expect(fiber).toContain('```\n\nRegister a cleanup-aware effect on this fiber.') expect(fiber).toContain('- `execute` — the effect body; see `Effect` for accepted shapes.') expect(fiber).toContain('**Returns** a disposer that tears the effect down and settles once done.') @@ -39,7 +39,7 @@ describe('Cordis core API generation', () => { mkdirSync(join(root, 'vendor/cordis/src'), { recursive: true }) writeFileSync(join(root, 'vendor/cordis/src/service.ts'), 'export class Service {\n run(): string { return "ok" }\n}\n') const page: CordisCoreApiPage = { - out: 'docs/cordis-catalog/core/service.md', + out: 'docs/cordis-api/service.md', title: 'Service', intro: 'Service API.', sections: [{ kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' }], diff --git a/scripts/cordis-core-api.ts b/scripts/cordis-core-api.ts index a2400fdb54..a32c1ce8fa 100644 --- a/scripts/cordis-core-api.ts +++ b/scripts/cordis-core-api.ts @@ -26,7 +26,7 @@ export interface CordisCoreApiPage { /** Explicit editorial grouping for the pinned Cordis core surface. */ export const CORDIS_CORE_API_PAGES: CordisCoreApiPage[] = [ { - out: 'docs/cordis-catalog/core/context.md', + out: 'docs/cordis-api/context.md', title: 'Context', intro: 'The context is the core Cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods are documented on [Events](events.md), effects and the current fiber on [Fiber](fiber.md), and plugin loading on [Registry](registry.md).', sections: [ @@ -35,9 +35,9 @@ export const CORDIS_CORE_API_PAGES: CordisCoreApiPage[] = [ ], }, { - out: 'docs/cordis-catalog/core/events.md', + out: 'docs/cordis-api/events.md', title: 'Events', - intro: 'The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../events.md).', + intro: 'The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated into each owning [subsystem page](../subsystems/core.md).', sections: [ { kind: 'context-merge', file: 'vendor/cordis/src/events.ts' }, { kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'EventOptions' }, @@ -45,7 +45,7 @@ export const CORDIS_CORE_API_PAGES: CordisCoreApiPage[] = [ ], }, { - out: 'docs/cordis-catalog/core/fiber.md', + out: 'docs/cordis-api/fiber.md', title: 'Fiber', intro: 'A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber, and `ctx.effect()` delegates to it.', sections: [ @@ -59,7 +59,7 @@ export const CORDIS_CORE_API_PAGES: CordisCoreApiPage[] = [ ], }, { - out: 'docs/cordis-catalog/core/registry.md', + out: 'docs/cordis-api/registry.md', title: 'Registry', intro: 'Plugin loading and dependency injection.', sections: [ @@ -69,7 +69,7 @@ export const CORDIS_CORE_API_PAGES: CordisCoreApiPage[] = [ ], }, { - out: 'docs/cordis-catalog/core/service.md', + out: 'docs/cordis-api/service.md', title: 'Service', intro: 'The base class for context services. A subclass loaded as a plugin registers itself as `ctx.`.', sections: [ @@ -357,7 +357,7 @@ function declarationPaste(ctx: RenderContext, rel: string, symbol: string): { do function sourceLink(source: string): string { const [file, line] = source.split(':') - return `[Source](../../../${file}${line === undefined ? '' : `#L${line}`})` + return `[Source](../../${file}${line === undefined ? '' : `#L${line}`})` } function unlink(text: string): string { diff --git a/scripts/cordis-walk.ts b/scripts/cordis-walk.ts index e414b572d3..0111d1d042 100644 --- a/scripts/cordis-walk.ts +++ b/scripts/cordis-walk.ts @@ -1,7 +1,39 @@ -/** Locate the Cordis module merge used by the vendored core API projector. */ +/** + * AST helpers shared by the Cordis generators: locate the Cordis module merge + * in a source file and enumerate the `interface Context` keys it declares. + * The vendored core API projector consumes the merge body; the per-subsystem + * region generator's exhaustiveness backstop consumes the key scan. + */ +import { globSync, readFileSync } from 'node:fs' +import { resolve, sep } from 'node:path' import ts from 'typescript' +/** + * Parse every file matching `pattern` (repo-relative, sorted, `/`-normalized) + * that textually mentions `interface Context`, yielding each file's cordis + * module-merge body. Files without a merge are skipped. + * @param scanRoot - Repository root the pattern is resolved against. + * @param pattern - Glob selecting the TypeScript files to scan. + * @returns One entry per file with a cordis module merge, in path order. + */ +export function contextMergeFiles( + scanRoot: string, + pattern: string, +): { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] { + const out: { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] = [] + for (const rel of globSync(pattern, { cwd: scanRoot }).map(s => s.split(sep).join('/')).sort()) { + const abs = resolve(scanRoot, rel) + const text = readFileSync(abs, 'utf8') + if (!text.includes('interface Context')) continue + const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true) + const body = cordisModuleBody(sf) + if (!body) continue + out.push({ rel, sf, text, body }) + } + return out +} + /** The body of the cordis module merge in `sf`: `declare module 'cordis'` * (harness packages) or `declare module './context.ts'` (vendor core), or * null when the file has neither. */ @@ -13,3 +45,22 @@ export function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null { } return null } + +/** + * Every `key: Type` property a `declare module 'cordis'` Context merge + * declares in one module body. + * @param body - The cordis module augmentation block. + * @param sf - Owning source file (for text extraction). + * @returns key → declared type-name text, in declaration order. + */ +export function contextKeyMap(body: ts.ModuleBlock, sf: ts.SourceFile): Map { + const keyToType = new Map() + for (const stmt of body.statements) { + if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue + for (const member of stmt.members) { + if (!ts.isPropertySignature(member) || !member.type) continue + keyToType.set(member.name.getText(sf), member.type.getText(sf)) + } + } + return keyToType +} diff --git a/scripts/gen-config-catalog.ts b/scripts/gen-config-catalog.ts index ebceec916b..a79ffdd47f 100644 --- a/scripts/gen-config-catalog.ts +++ b/scripts/gen-config-catalog.ts @@ -819,7 +819,7 @@ export function render(entries: CatalogEntry[]): string { '', '# Plugin Config Catalog', '', - 'Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin\'s full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the cordis [events](cordis-catalog/events.md) + [services](cordis-catalog/services.md) catalogs, the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference.', + 'Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin\'s full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the generated `cordis-surface` region on each [subsystem page](subsystems/core.md), the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference.', '', 'This file is GENERATED from source (`scripts/gen-config-catalog.ts`) and verified fresh by `pnpm run verify-config-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks use a `ts config-catalog` fence (skipped by doc-typecheck, since a lone declaration referencing imports is not standalone-compilable). The generator also cross-checks the runtime schemastery schema against the pasted declaration — every schema-validated key, nested keys included, must be locatable on the declared config type — so the paste cannot hide a loader-accepted field.', '', diff --git a/scripts/gen-cordis-catalog-record.spec.ts b/scripts/gen-cordis-catalog-record.spec.ts new file mode 100644 index 0000000000..73720e8be8 --- /dev/null +++ b/scripts/gen-cordis-catalog-record.spec.ts @@ -0,0 +1,144 @@ +/** + * Negative-path coverage for the guarded pair auto-record + * (`maybeRecordPair`): the safety property is that regeneration re-records a + * pair's `.i18n.yaml` ONLY for a region-confined write over a well-formed, + * previously-consistent record — every other state is left for the pairing + * gate to report. + */ + +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { maybeRecordPair, REGION_BEGIN, REGION_END, spliceRegion } from './gen-cordis-catalog.ts' +import { blobHash, renderPairMeta } from './translation-pairing.ts' + +const PAGE = 'docs/subsystems/fix.md' +const ZH = 'docs/subsystems/fix.zh.md' +const META = 'docs/subsystems/fix.i18n.yaml' + +function page(prose: string, region: string): string { + return `# Fix\n\n${prose}\n\n${REGION_BEGIN}\n${region}\n${REGION_END}\n` +} + +const roots: string[] = [] +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +/** Lay out a pair on disk and return { root, before } for a regeneration that already wrote `current`. */ +function setup(options: { + beforeEn: string + beforeZh: string + currentEn: string + currentZh: string + meta?: string | null + omitZhSnapshot?: boolean +}): { root: string; before: Map } { + const root = mkdtempSync(join(tmpdir(), 'record-guard-')) + roots.push(root) + mkdirSync(join(root, 'docs/subsystems'), { recursive: true }) + writeFileSync(join(root, PAGE), options.currentEn) + writeFileSync(join(root, ZH), options.currentZh) + const meta = options.meta === undefined + ? renderPairMeta(PAGE, blobHash(Buffer.from(options.beforeEn)), ZH, blobHash(Buffer.from(options.beforeZh))) + : options.meta + if (meta !== null) writeFileSync(join(root, META), meta) + const before = new Map([[PAGE, Buffer.from(options.beforeEn)]]) + if (!options.omitZhSnapshot) before.set(ZH, Buffer.from(options.beforeZh)) + return { root, before } +} + +describe('maybeRecordPair', () => { + const beforeEn = page('prose.', 'old region') + const beforeZh = page('散文。', 'old region') + const currentEn = page('prose.', 'new region') + const currentZh = page('散文。', 'new region') + + it('re-records a region-confined write over a consistent record', () => { + const { root, before } = setup({ beforeEn, beforeZh, currentEn, currentZh }) + expect(maybeRecordPair(PAGE, before, root)).toBe(true) + expect(readFileSync(join(root, META), 'utf8')) + .toBe(renderPairMeta(PAGE, blobHash(Buffer.from(currentEn)), ZH, blobHash(Buffer.from(currentZh)))) + }) + + it('refuses when the pair was already out of sync before the run', () => { + const stale = renderPairMeta(PAGE, blobHash(Buffer.from('drifted long ago\n')), ZH, blobHash(Buffer.from(beforeZh))) + const { root, before } = setup({ beforeEn, beforeZh, currentEn, currentZh, meta: stale }) + expect(maybeRecordPair(PAGE, before, root)).toBe(false) + expect(readFileSync(join(root, META), 'utf8')).toBe(stale) + }) + + it('refuses a malformed record even when its hashes are current', () => { + // A renamed key with preserved hashes must stay the pairing gate's error, + // never become valid through regeneration. + const renamedKeys = [ + '# comment', + `fixXmd: ${blobHash(Buffer.from(beforeEn))}`, + `fix.zh.md: ${blobHash(Buffer.from(beforeZh))}`, + '', + ].join('\n') + const { root, before } = setup({ beforeEn, beforeZh, currentEn, currentZh, meta: renamedKeys }) + expect(maybeRecordPair(PAGE, before, root)).toBe(false) + expect(readFileSync(join(root, META), 'utf8')).toBe(renamedKeys) + }) + + it('refuses a record with extra entries', () => { + const extra = renderPairMeta(PAGE, blobHash(Buffer.from(beforeEn)), ZH, blobHash(Buffer.from(beforeZh))) + + `other.md: ${blobHash(Buffer.from(beforeEn))}\n` + const { root, before } = setup({ beforeEn, beforeZh, currentEn, currentZh, meta: extra }) + expect(maybeRecordPair(PAGE, before, root)).toBe(false) + }) + + it('refuses a record with a duplicated expected key', () => { + // Map#set would collapse the duplicate back to size 2; the parser must + // reject the repeat instead of letting the guard accept the record. + const duplicated = [ + `fix.md: ${blobHash(Buffer.from(beforeEn))}`, + `fix.md: ${blobHash(Buffer.from(beforeEn))}`, + `fix.zh.md: ${blobHash(Buffer.from(beforeZh))}`, + '', + ].join('\n') + const { root, before } = setup({ beforeEn, beforeZh, currentEn, currentZh, meta: duplicated }) + expect(maybeRecordPair(PAGE, before, root)).toBe(false) + expect(readFileSync(join(root, META), 'utf8')).toBe(duplicated) + }) + + it('refuses when prose drifted alongside the region write', () => { + const proseDrift = page('prose, edited by a human.', 'new region') + const { root, before } = setup({ beforeEn, beforeZh, currentEn: proseDrift, currentZh }) + expect(maybeRecordPair(PAGE, before, root)).toBe(false) + }) + + it('refuses a brand-new pair with no record', () => { + const { root, before } = setup({ beforeEn, beforeZh, currentEn, currentZh, meta: null }) + expect(maybeRecordPair(PAGE, before, root)).toBe(false) + }) + + it('refuses when a side has no pre-write snapshot', () => { + const { root, before } = setup({ beforeEn, beforeZh, currentEn, currentZh, omitZhSnapshot: true }) + expect(maybeRecordPair(PAGE, before, root)).toBe(false) + }) +}) + +describe('spliceRegion', () => { + it('replaces exactly the cordis-surface region', () => { + const doc = `# T\n\nprose\n\n${REGION_BEGIN}\nold\n${REGION_END}\ntail\n` + expect(spliceRegion(doc, `${REGION_BEGIN}\nnew\n${REGION_END}`)) + .toBe(`# T\n\nprose\n\n${REGION_BEGIN}\nnew\n${REGION_END}\ntail\n`) + }) + + it('fails loud on a page carrying only some other generator\'s region', () => { + // Another generator's markers satisfy the generic region grammar but must + // never be overwritten by THIS generator's splice. + const foreign = '# T\n\n\ntheirs\n\n' + expect(() => spliceRegion(foreign, `${REGION_BEGIN}\nnew\n${REGION_END}`)) + .toThrow('expected exactly 1 cordis-surface region, found 0 BEGIN/0 END') + }) + + it('fails loud on duplicate cordis-surface markers', () => { + const doubled = `${REGION_BEGIN}\na\n${REGION_END}\n${REGION_BEGIN}\nb\n${REGION_END}\n` + expect(() => spliceRegion(doubled, `${REGION_BEGIN}\nnew\n${REGION_END}`)) + .toThrow('found 2 BEGIN/2 END') + }) +}) diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 3cc277152e..120904d4b5 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -1,24 +1,156 @@ /** - * Generate committed Cordis artifacts from the Typert catalog projector and - * the independent vendored-core projector. + * Generate the per-subsystem Cordis service/event reference regions from the + * Typert catalog projection. Every harness `ctx.` service and event scope + * maps to exactly one `docs/subsystems/` page through the curated tables below; + * the generator injects each page's surface between its GENERATED markers — + * byte-identically into both language sides of the pair — and re-records a + * pair's `.i18n.yaml` only when nothing outside the region changed. The + * projection enforces event modes, JSDoc parameter/return completeness, and + * signature type-link coverage; the inherited (vendor) tier renders to + * `docs/cordis-api/inherited.md`. `--check` verifies every generated artifact. */ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { projectCordisCatalog, - renderEvents, - renderServices, + renderInheritedPage, + renderPageRegion, + REGION_BEGIN, + REGION_END, } from '@deepseek-ai/dsh-typert-generator' import type { CordisCatalogPolicy } from '@deepseek-ai/dsh-typert-generator' import { renderCordisCoreApiPages } from './cordis-core-api.ts' +import { contextKeyMap, contextMergeFiles } from './cordis-walk.ts' +import { + blobHash, + parsePairMeta, + partitionGeneratedRegions, + renderPairMeta, +} from './translation-pairing.ts' const root = resolve(import.meta.dirname, '..') -const OUT_EVENTS = 'docs/cordis-catalog/events.md' -const OUT_SERVICES = 'docs/cordis-catalog/services.md' +const SUBSYSTEMS_DIR = 'docs/subsystems' +const OUT_INHERITED = 'docs/cordis-api/inherited.md' const OUT_RUNTIME_API = 'packages/self-modification/tool-cordis/src/api-catalog.ts' -/** One primary subsystems page per project type used by a generated signature. */ +export { REGION_BEGIN, REGION_END } + +/** + * The owning subsystems page for every harness `ctx.` service the + * projection discovers. Fail-closed both ways: a discovered key absent here + * and an entry whose key the projection no longer discovers are both hard + * errors, so the partition can never silently drift from the service surface. + */ +export const SERVICE_PAGE: Record = { + agentLoop: 'core.md', + agents: 'core.md', + approval: 'approval.md', + bash: 'bash.md', + bashEnv: 'bash.md', + clientModuleHost: 'client-modules.md', + codeRuntime: 'code-runtime.md', + commands: 'commands.md', + compact: 'compaction.md', + credentials: 'credentials.md', + directoryPicker: 'workspace.md', + e2b: 'subprocess.md', + fs: 'filesystem.md', + goals: 'goal.md', + httpServer: 'http-server.md', + invariants: 'invariants.md', + llm: 'llm-streaming.md', + permission: 'permission.md', + planMode: 'plan.md', + pty: 'pty.md', + sandbox: 'sandbox.md', + sandboxPolicy: 'sandbox.md', + sessionPersistence: 'persistence.md', + sessionQuery: 'session-query.md', + sessionReferences: 'session-reference.md', + sessionProjectionCache: 'session-projection.md', + sessionProjections: 'session-projection.md', + sessions: 'session.md', + settings: 'settings.md', + sessionTitle: 'session-title.md', + skills: 'skills.md', + spillStore: 'spill.md', + storage: 'storage.md', + storageDomain: 'storage.md', + subagents: 'subagent.md', + subprocess: 'subprocess.md', + systemPrompt: 'system-prompt.md', + tasks: 'tasks.md', + telemetry: 'telemetry.md', + tokenMeter: 'token-meter.md', + toolResultPrune: 'compaction.md', + tools: 'tools.md', + typert: 'typert.md', + typertGateway: 'typert.md', + userInteraction: 'user-interaction.md', + web: 'web.md', + workflows: 'workflow.md', + workspace: 'workspace.md', +} + +/** + * Context keys declared in `interface Context` merges that the rendering + * projection cannot see, each with the reason and its documentation owner. + * The scan that enforces this list reads EVERY `declare module 'cordis'` + * Context merge under `packages/x/x/src/*.ts` — not only root `index.ts` + * files with a same-named service class — so a new service can never silently + * join this blind spot: it either enters {@link SERVICE_PAGE} or names itself + * here. + * TODO(cordis-catalog-interface-services): the interface-typed and + * non-index-declared entries would all render once the projection resolves a + * Context key through its declaring file's imports to the class declaration. + */ +export const SERVICE_WALK_EXEMPTIONS: Record = { + agent: 'not a service: the DX accessor field on Agent.ctx (root accessor defaulting to undefined) — docs/subsystems/core.md owns the Agent handle', + configuredAgentIdentities: 'not a service: launcher-provided boot-context value (ConfiguredAgentIdentities | undefined) — packages/core/agent-loop/README.md owns the launcher contract', + launcherSessionQueryPath: 'not a service: launcher-provided boot-context value (string | undefined) — packages/session-query/session-query-sqlite/README.md owns the launcher contract', + dshHomePath: 'not a service: boot-provided root accessor function (typeof dshHomePath | undefined) for Loader !!js config expressions — packages/boot/app-boot/README.md owns the boot contract', + headlessIo: 'not a service: launcher-provided root accessor value (HeadlessIo | undefined) for the headless bundle runner — packages/bundle/headless/README.md owns the launcher contract', + launcherEnvironment: 'not a service: launcher-provided root accessor value (EnvironmentSnapshot | undefined) — packages/util/environment/README.md owns the launcher contract', + lsp: 'interface-typed (LspService); implementing class Lsp is not the declared type name — packages/lsp/lsp/README.md owns the surface', + apiProxy: 'interface-typed (ApiProxy) with the class in api-proxy.ts, not index.ts — packages/host/apiproxy/README.md owns the surface', + appShell: 'client-side interface-typed browser service — packages/client/web/README.md owns the surface', + connection: 'client-side interface-typed browser service — packages/client/connection/README.md owns the surface', +} + +/** + * The owning subsystems page for every harness event scope (the segment + * before the first `/`). Fail-closed exactly like {@link SERVICE_PAGE}. + * `slash` lives with the human-command surface: the client slash-input + * protocol parses toward command invocation and `dsh-ui-slash` owns the + * declarations, but commands.md owns the cross-package command story. + */ +export const EVENT_SCOPE_PAGE: Record = { + 'agent': 'core.md', + 'agent-loop': 'core.md', + 'approval': 'approval.md', + 'commands': 'commands.md', + 'credentials': 'credentials.md', + 'domain': 'storage.md', + 'fs': 'filesystem.md', + 'goal': 'goal.md', + 'llm': 'llm-streaming.md', + 'session': 'session.md', + 'settings': 'settings.md', + 'skills': 'skills.md', + 'subagent': 'subagent.md', + 'system-prompt': 'system-prompt.md', + 'telemetry': 'telemetry.md', + 'tools': 'tools.md', + 'workflow': 'workflow.md', +} + +/** + * One primary subsystems page per project type used by a generated + * signature. This stays curated because union names intentionally do not + * reuse the type-equivalence manifest's map-symbol entries and some symbols + * appear on more than one page. + */ export const LINK_MAP: Readonly> = { Agent: 'core.md', AgentCancelCause: 'core.md', @@ -94,12 +226,12 @@ export const LINK_MAP: Readonly> = { FsWriteIntent: 'filesystem.md', FsWriteOutcome: 'filesystem.md', CreateGoalRequest: 'goal.md', - CreateGoalResult: 'goal.md', EditGoalRequest: 'goal.md', GoalBlockReason: 'goal.md', GoalChanged: 'goal.md', GoalRef: 'goal.md', GoalView: 'goal.md', + CreateGoalResult: 'goal.md', CommandDefinition: 'commands.md', CommandDescriptor: 'commands.md', CommandResult: 'commands.md', @@ -248,6 +380,18 @@ export const LINK_MAP: Readonly> = { TelemetryRecord: 'telemetry.md', WorkflowRunInfo: 'workflow.md', WorkflowStartRequest: 'workflow.md', + ProjectionDefinition: 'session-projection.md', + SessionProjectionMap: 'session-projection.md', + ProjectionChangeListener: 'session-projection.md', + ProjectionSnapshot: 'session-projection.md', + ProjectionCheckpoint: 'session-projection.md', + DirectoryPickerCapability: 'workspace.md', + TypertContribution: 'invariants.md', + TypertFace: 'invariants.md', + TypertPackageFilter: 'invariants.md', + TypertPackageRecord: 'invariants.md', + TypertSchemaFilter: 'invariants.md', + TypertSchemaRecord: 'invariants.md', } /** TypeScript lib and pinned framework types with no repository-owned data page. */ @@ -277,31 +421,19 @@ export const TYPE_LINK_EXEMPTIONS: Readonly> = { 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', - DirectoryPickerCapability: 'picker interaction contract is owned by packages/host/directory-picker/README.md', 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', - ProjectionDefinition: 'projection unit contract is owned by packages/session/session-projection/README.md', - SessionProjectionMap: 'merge-extensible projection key map is owned by packages/session/session-projection/src/types.ts', - ProjectionChangeListener: 'change-feed listener contract is owned by packages/session/session-projection/src/index.ts', - ProjectionSnapshot: 'watermark snapshot shape is owned by packages/session/session-projection/src/index.ts', - ProjectionCheckpoint: 'persisted checkpoint row map is owned by packages/session/session-projection/src/index.ts', CommandExecution: 'executor return contract is owned by packages/interaction/commands/src/index.ts', - TypertContribution: 'registry contribution contract is owned by packages/typert/registry/README.md', - TypertFace: 'registry face identity is owned by packages/typert/registry/README.md', - TypertPackageFilter: 'registry package query filter is owned by packages/typert/registry/README.md', - TypertPackageRecord: 'registry package record is owned by packages/typert/registry/README.md', - TypertSchemaFilter: 'registry schema query filter is owned by packages/typert/registry/README.md', - TypertSchemaRecord: 'registry schema record is owned by packages/typert/registry/README.md', - TypeRTDisposer: 'TypeRT lifecycle contract is owned by packages/typert/type-meta/README.md', 'z.core.JSONSchema.BaseSchema': 'zod projection output is owned by the zod v4 API', 'z.core.ToJSONSchemaParams': 'zod projection parameters are owned by the zod v4 API', + TypeRTDisposer: 'TypeRT lifecycle contract is owned by packages/typert/type-meta/README.md', + InvokeRemoteRequest: 'gateway invocation contract is owned by packages/api/gateway/README.md', LocaleDict: 'service-local dictionary shape is owned by packages/client/i18n/src/index.ts', ThemeTokens: 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts', Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts', WebUpgradeRoute: 'upgrade route registration contract is owned by packages/host/webserver/src/index.ts', InvariantRegistration: 'service-local lifecycle handle is owned by packages/support/invariants/README.md', - InvokeRemoteRequest: 'gateway invocation contract is owned by packages/api/gateway/README.md', 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', @@ -351,15 +483,171 @@ export const CORDIS_CATALOG_POLICY: CordisCatalogPolicy = { ], } -/** CLI entry: default writes every artifact; `--check` reports stale files. + +/** + * Splice a page's generated cordis-surface region into its Markdown content. + * The page must contain exactly one cordis-surface region (the markers are + * part of the hand-owned page skeleton once, then owned by the generator); + * zero or several is a partition error the caller reports with the page path. + * The match is on THIS generator's exact markers, not the generic region + * grammar, so a page carrying only some other generator's region fails loud + * instead of having that region overwritten. + * @param content - the page's current full Markdown text. + * @param region - the freshly rendered marker-delimited region. + * @returns the page text with the region replaced. + */ +export function spliceRegion(content: string, region: string): string { + const lines = content.split('\n') + const begins = lines.flatMap((line, index) => (line === REGION_BEGIN ? [index] : [])) + const ends = lines.flatMap((line, index) => (line === REGION_END ? [index] : [])) + if (begins.length !== 1 || ends.length !== 1) { + throw new Error(`expected exactly 1 cordis-surface region, found ${begins.length} BEGIN/${ends.length} END; add the BEGIN/END cordis-surface markers once`) + } + const begin = begins[0] ?? -1 + const end = ends[0] ?? -1 + if (end < begin) throw new Error('cordis-surface END marker precedes its BEGIN') + return [...lines.slice(0, begin), ...region.split('\n'), ...lines.slice(end + 1)].join('\n') +} + +/** + * Compute every generated artifact: the inherited-tier page, the model-facing + * runtime API module, plus, per mapped subsystems page, the pair's two updated + * documents with the injected region. Fail-loud partition checks live here: an + * unmapped service/event scope, a mapping whose page file does not exist, a + * curated entry whose key/scope the projection no longer discovers, and a + * mapped page missing its markers are all aggregated errors. + * @returns `[repo-relative path, exact content]` for every generated artifact. + */ +export function computeOutputs(): [string, string][] { + const { projector, model } = projectCordisCatalog(root, CORDIS_CATALOG_POLICY) + const services = [...model.services] + const events = [...model.events] + const problems: string[] = [] + + const discoveredKeys = new Set(services.map(s => s.key)) + const discoveredScopes = new Set(events.map(e => e.scope)) + for (const s of services) { + if (!Object.hasOwn(SERVICE_PAGE, s.key)) problems.push(`service ctx.${s.key} (${s.source}) has no SERVICE_PAGE entry; every service maps to exactly one subsystems page.`) + } + for (const scope of discoveredScopes) { + if (!Object.hasOwn(EVENT_SCOPE_PAGE, scope)) problems.push(`event scope '${scope}/*' has no EVENT_SCOPE_PAGE entry; every event scope maps to exactly one subsystems page.`) + } + for (const key of Object.keys(SERVICE_PAGE)) { + if (!discoveredKeys.has(key)) problems.push(`SERVICE_PAGE maps 'ctx.${key}' but the projection discovers no such service; remove the stale entry.`) + } + for (const scope of Object.keys(EVENT_SCOPE_PAGE)) { + if (!discoveredScopes.has(scope)) problems.push(`EVENT_SCOPE_PAGE maps '${scope}/*' but the projection discovers no such scope; remove the stale entry.`) + } + // The rendering projection only sees a Context key it can resolve to a + // documented service class. This independent scan reads EVERY Context merge + // so a key the projection cannot render must either be rendered (mapped) or + // carry a named SERVICE_WALK_EXEMPTIONS reason — never vanish silently. + const declaredKeys = new Map() + for (const { rel, sf, body } of contextMergeFiles(root, 'packages/*/*/src/*.ts')) { + for (const key of contextKeyMap(body, sf).keys()) { + if (!declaredKeys.has(key)) declaredKeys.set(key, rel) + } + } + for (const [key, rel] of declaredKeys) { + const rendered = discoveredKeys.has(key) + const exempt = Object.hasOwn(SERVICE_WALK_EXEMPTIONS, key) + if (!rendered && !exempt) { + problems.push(`ctx.${key} (${rel}) is declared in a Context merge but invisible to the rendering projection; map it in SERVICE_PAGE (after making it renderable) or name it in SERVICE_WALK_EXEMPTIONS with its documentation owner.`) + } + if (rendered && exempt) problems.push(`ctx.${key} is rendered by the projection but still listed in SERVICE_WALK_EXEMPTIONS; remove the stale exemption.`) + } + for (const key of Object.keys(SERVICE_WALK_EXEMPTIONS)) { + if (!declaredKeys.has(key)) problems.push(`SERVICE_WALK_EXEMPTIONS names 'ctx.${key}' but no Context merge declares it; remove the stale exemption.`) + } + if (problems.length > 0) throw new Error(`gen-cordis-catalog: ${problems.length} partition violation(s):\n${problems.map(p => ` ${p}`).join('\n')}`) + + const pages = [...new Set([...Object.values(SERVICE_PAGE), ...Object.values(EVENT_SCOPE_PAGE)])].sort() + const outputs: [string, string][] = [ + [OUT_INHERITED, renderInheritedPage(CORDIS_CATALOG_POLICY)], + [OUT_RUNTIME_API, projector.renderRuntimeApi(model)], + ] + for (const page of pages) { + const region = renderPageRegion( + page, + services.filter(s => SERVICE_PAGE[s.key] === page), + events.filter(e => EVENT_SCOPE_PAGE[e.scope] === page), + CORDIS_CATALOG_POLICY, + ) + for (const side of [page, page.replace(/\.md$/, '.zh.md')]) { + const rel = `${SUBSYSTEMS_DIR}/${side}` + let current: string + try { + current = readFileSync(resolve(root, rel), 'utf8') + } catch { + // Both pair sides must exist before a region can be injected; the + // pairing gate owns pair completeness, this generator names the miss. + problems.push(`${rel}: mapped subsystems page does not exist.`) + continue + } + try { + outputs.push([rel, spliceRegion(current, region)]) + } catch (error) { + problems.push(`${rel}: ${error instanceof Error ? error.message : String(error)}`) + } + } + } + if (problems.length > 0) throw new Error(`gen-cordis-catalog: ${problems.length} page violation(s):\n${problems.map(p => ` ${p}`).join('\n')}`) + return outputs +} + +/** + * Re-record a pair's `.i18n.yaml` after a region write ONLY when the write is + * region-confined: both sides' region-stripped content must be byte-equal to + * the region-stripped previous content whose hashes the record holds. The + * caller supplies the previous bytes (read before writing); human-content + * drift leaves the record untouched so the pairing gate still demands the + * normal translation flow. + * @param pageRel - repo-relative English page path (`docs/subsystems/x.md`). + * @param before - pre-write bytes per repo-relative path. + * @param scanRoot - repository root override for tests. + * @returns true when the record was refreshed. + */ +export function maybeRecordPair(pageRel: string, before: Map, scanRoot: string = root): boolean { + const zhRel = pageRel.replace(/\.md$/, '.zh.md') + const metaRel = pageRel.replace(/\.md$/, '.i18n.yaml') + const metaAbs = resolve(scanRoot, metaRel) + let meta: string + try { + meta = readFileSync(metaAbs, 'utf8') + } catch { + // No record yet: a brand-new pair is recorded by the author's --write + // after review, never silently by regeneration. + return false + } + // The record must be exactly the well-formed two-entry shape for THIS pair; + // a malformed or renamed-key sidecar is the pairing gate's problem to + // report, never something regeneration silently repairs into validity. + const recorded = parsePairMeta(meta) + const names = [pageRel, zhRel].map(rel => rel.split('/').at(-1) ?? rel) + if (!recorded || recorded.size !== 2 || !names.every(name => recorded.has(name))) return false + for (const rel of [pageRel, zhRel]) { + const previous = before.get(rel) + if (!previous) return false + if (recorded.get(rel.split('/').at(-1) ?? rel) !== blobHash(previous)) return false + const current = readFileSync(resolve(scanRoot, rel)) + const strippedBefore = partitionGeneratedRegions(previous.toString('utf8')).stripped + const strippedAfter = partitionGeneratedRegions(current.toString('utf8')).stripped + if (strippedBefore !== strippedAfter) return false + } + const source = readFileSync(resolve(scanRoot, pageRel)) + const zh = readFileSync(resolve(scanRoot, zhRel)) + writeFileSync(metaAbs, renderPairMeta(pageRel, blobHash(source), zhRel, blobHash(zh))) + return true +} + +/** CLI entry: default regenerates every artifact, `--check` fails if any is + * stale. Guarded behind an entry-point check so importing this module for + * tests neither regenerates the committed files nor calls process.exit. * @returns nothing; writes files or reports freshness through the process. */ export function main(): void { - const { projector, model } = projectCordisCatalog(root, CORDIS_CATALOG_POLICY) const outputs: [string, string][] = [ - [OUT_EVENTS, renderEvents([...model.events], CORDIS_CATALOG_POLICY)], - [OUT_SERVICES, renderServices([...model.services], CORDIS_CATALOG_POLICY)], - [OUT_RUNTIME_API, projector.renderRuntimeApi(model)], + ...computeOutputs(), ...renderCordisCoreApiPages(), ] if (process.argv.includes('--check')) { @@ -369,25 +657,51 @@ export function main(): void { try { committed = readFileSync(resolve(root, out), 'utf8') } catch { - // Only ENOENT is expected; either read failure has the same remedy. + // Only ENOENT (not yet generated) is expected; a present-but-unreadable + // file is not a state this repo produces. Either way the remedy is the + // same — regenerate — so treat a read failure as "stale". committed = null } if (committed !== content) stale.push(out) } if (stale.length === 0) { - console.log(`gen-cordis-catalog: ${outputs.length} generated file(s) are up to date.`) + console.log(`gen-cordis-catalog: ${outputs.length} generated file(s)/region(s) are up to date.`) process.exit(0) } - console.error(`gen-cordis-catalog: ${stale.join(' and ')} ${stale.length === 1 ? 'is' : 'are'} stale. Run \`pnpm run gen-cordis-catalog\` and commit the result.`) + console.error(`gen-cordis-catalog: stale — ${stale.join(', ')}. Run \`pnpm run gen-cordis-catalog\` and commit the result.`) process.exit(1) } + const before = new Map() + for (const [out] of outputs) { + try { + before.set(out, readFileSync(resolve(root, out))) + } catch { + // First generation of this artifact; nothing to guard, nothing to record. + } + } + let changedPages = 0 + let recorded = 0 for (const [out, content] of outputs) { const destination = resolve(root, out) + if (before.get(out)?.toString('utf8') === content) continue mkdirSync(dirname(destination), { recursive: true }) writeFileSync(destination, content) + changedPages++ } - console.log(`gen-cordis-catalog: wrote ${outputs.length} generated file(s).`) + for (const page of [...new Set([...Object.values(SERVICE_PAGE), ...Object.values(EVENT_SCOPE_PAGE)])]) { + const rel = `${SUBSYSTEMS_DIR}/${page}` + const zhRel = rel.replace(/\.md$/, '.zh.md') + const wroteEither = [rel, zhRel].some((side) => { + const previous = before.get(side) + return previous !== undefined && previous.toString('utf8') !== readFileSync(resolve(root, side), 'utf8') + }) + if (wroteEither && maybeRecordPair(rel, before)) recorded++ + } + console.log(`gen-cordis-catalog: ${outputs.length} artifact(s) computed, ${changedPages} written, ${recorded} pair record(s) refreshed.`) } -if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) main() +// Run only when invoked as a script, not when imported by a test. +if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) { + main() +} diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 4f72b1d953..1c4cce0fa3 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -1344,7 +1344,7 @@ function renderIndex(docs: GraphDoc[]): string { const maintenance = 'mixed: each linked page declares generated, hybrid, or curated mode' return [ ...generatedHeader('Documentation Graph Index'), - 'These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the generated [events](cordis-catalog/events.md) / [services](cordis-catalog/services.md) catalogs, [tool-catalog.md](tool-catalog.md), and [subsystems/](subsystems/core.md).', + 'These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the [subsystem pages](subsystems/core.md) (types + the generated `cordis-surface` regions) and [tool-catalog.md](tool-catalog.md).', '', 'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md).', '', diff --git a/scripts/gen-persistence-catalog.ts b/scripts/gen-persistence-catalog.ts index 800497ddb7..5294242e27 100644 --- a/scripts/gen-persistence-catalog.ts +++ b/scripts/gen-persistence-catalog.ts @@ -352,7 +352,7 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv '', '# Session Persistence Event Catalog', '', - 'Every event type that can appear in a session\'s durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](subsystems/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](subsystems/persistence.md) (how the log is made durable), and the [cordis events catalog](cordis-catalog/events.md) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).', + 'Every event type that can appear in a session\'s durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](subsystems/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](subsystems/persistence.md) (how the log is made durable), and the generated region of [session.md](subsystems/session.md#cordis-surface) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).', '', 'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).', '', diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 39ed4d7620..a68ab874f0 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -608,7 +608,7 @@ export function render(catalog: ToolCatalog): string { '', '# Tool Schema Catalog', '', - 'Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the cordis [events](cordis-catalog/events.md) & [services](cordis-catalog/services.md) catalogs (the wiring a plugin listens to and calls) and [subsystems/](subsystems/core.md) (the types those signatures move) — this page is the *tools* the agent is offered.', + 'Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the [subsystem pages](subsystems/core.md) (the types plus each page\'s generated `cordis-surface` wiring region) — this page is the *tools* the agent is offered.', '', 'This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator\'s boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog Agent Note](../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md).', '', diff --git a/scripts/project-doc-site.spec.ts b/scripts/project-doc-site.spec.ts index ebdce88f6d..c2ca4136d3 100644 --- a/scripts/project-doc-site.spec.ts +++ b/scripts/project-doc-site.spec.ts @@ -276,7 +276,7 @@ describe('docsPages locale routes', () => { const translated = rootPages.filter(page => page.contentLocale === 'zh-CN') const fallbacks = rootPages.filter(page => page.contentLocale === 'en-US') - expect(translated).toHaveLength(20) + expect(translated).toHaveLength(37) expect(translated.every(page => page.source.endsWith('.zh.md'))).toBe(true) expect(fallbacks.map(page => page.source).sort()).toEqual([ 'docs/subsystems/commands.md', @@ -286,11 +286,11 @@ describe('docsPages locale routes', () => { }) it('publishes the Cordis core API under matching locale structures', () => { - const files = ['context.md', 'events.md', 'fiber.md', 'registry.md', 'service.md'] + const files = ['context.md', 'events.md', 'fiber.md', 'registry.md', 'service.md', 'inherited.md'] for (const file of files) { const root = docsPages.find(page => page.route === `reference/cordis-api/${file}`) const english = docsPages.find(page => page.route === `en/reference/cordis-api/${file}`) - expect(root?.source).toBe(`docs/cordis-catalog/core/${file}`) + expect(root?.source).toBe(`docs/cordis-api/${file}`) expect(root?.section).toBe('Cordis API') expect(english?.source).toBe(root?.source) expect(english?.section).toBe('Cordis Core API') diff --git a/scripts/project-doc-site.ts b/scripts/project-doc-site.ts index e3397237a3..55c7204d78 100644 --- a/scripts/project-doc-site.ts +++ b/scripts/project-doc-site.ts @@ -131,6 +131,12 @@ function destinationRange(rawNode: string, type: 'link' | 'image' | 'definition' return { start, end: rawNode.length } } +// `#fragment` suffixes pass through verbatim. Generated cordis-surface +// headings carry explicit `` anchors with the GitHub slug, so those +// fragments resolve on the published site too; hand-written headings rely on +// VitePress's own slugger, which differs from GitHub's for punctuation-heavy +// text — hand-authored cross-page fragments should prefer plain-text headings +// or explicit anchors. function splitTarget(url: string): { path: string; suffix: string } { const boundary = url.search(/[?#]/) if (boundary === -1) return { path: url, suffix: '' } diff --git a/scripts/translation-pairing.manifest.json b/scripts/translation-pairing.manifest.json index 7c77929ba3..112cd3ae4d 100644 --- a/scripts/translation-pairing.manifest.json +++ b/scripts/translation-pairing.manifest.json @@ -7,7 +7,7 @@ "docs/agent-lifecycle.md", "docs/capability-seams.md", "docs/config-catalog.md", - "docs/cordis-catalog/", + "docs/cordis-api/", "docs/event-producer-consumer.md", "docs/graph-atlas.md", "docs/i18n/style-samples.md", diff --git a/scripts/translation-pairing.spec.ts b/scripts/translation-pairing.spec.ts index 1073495d08..b89df38f52 100644 --- a/scripts/translation-pairing.spec.ts +++ b/scripts/translation-pairing.spec.ts @@ -12,11 +12,13 @@ import { translationPairPaths, } from './translation-pairing-record.ts' import { + blobHash, isTranslationScopeFile, pairAnchorOfArgument, parseTranslationMarkdown, parseTranslationPairingCliArgs, parseTranslationPairingManifest, + partitionGeneratedRegions, translationStructureDiff, translationStructureSignature, } from './translation-pairing.ts' @@ -292,3 +294,42 @@ describe('pair CLI arguments', () => { expect(() => parseTranslationPairingCliArgs(['--cached', '--write', 'docs/foo.md'])).toThrow('read-only') }) }) + +describe('generated regions', () => { + const BEGIN = '' + const END = '' + + it('partitions marker-delimited regions from the hand-owned remainder', () => { + const doc = `# T\n\nprose\n\n${BEGIN}\ninjected\n${END}\ntail\n` + const { regions, stripped } = partitionGeneratedRegions(doc) + expect(regions).toEqual([`${BEGIN}\ninjected\n${END}`]) + expect(stripped).toBe('# T\n\nprose\n\ntail\n') + }) + + it('treats a document without markers as one hand-owned remainder', () => { + const { regions, stripped } = partitionGeneratedRegions('# T\n\nprose\n') + expect(regions).toEqual([]) + expect(stripped).toBe('# T\n\nprose\n') + }) + + it('rejects unbalanced or nested markers', () => { + expect(() => partitionGeneratedRegions(`${END}\n`)).toThrow('without a BEGIN') + expect(() => partitionGeneratedRegions(`${BEGIN}\n`)).toThrow('without an END') + expect(() => partitionGeneratedRegions(`${BEGIN}\n${BEGIN}\n${END}\n`)).toThrow('nested') + }) + + it('rejects mismatched slugs and malformed marker lines', () => { + expect(() => partitionGeneratedRegions('\nx\n\n')) + .toThrow("END slug 'b' does not match its BEGIN slug 'a'") + expect(() => partitionGeneratedRegions(' trailing\nx\n\n')) + .toThrow('malformed generated region marker line') + expect(() => partitionGeneratedRegions('x\n tail\n')) + .toThrow('malformed generated region marker line') + }) + + it('computes the exact git blob hash', () => { + // `git hash-object` of the empty file and of "x\n" — pinned upstream values. + expect(blobHash(Buffer.from(''))).toBe('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391') + expect(blobHash(Buffer.from('x\n'))).toBe('587be6b4c3f93f93c489c0111bba5596147a26cb') + }) +}) diff --git a/scripts/translation-pairing.ts b/scripts/translation-pairing.ts index 6d6893f843..417e836578 100644 --- a/scripts/translation-pairing.ts +++ b/scripts/translation-pairing.ts @@ -2,13 +2,122 @@ * Pure parsing and structural helpers for the bilingual-document pairing * gate. Kept separate from the CLI so corpus discovery and signature behavior * can be regression-tested without reading or mutating the repository tree. + * Also the one home of the generated-region grammar and the pair-record + * primitives, shared by the pairing gate and the region-injecting generators. */ +import { createHash } from 'node:crypto' +import { basename } from 'node:path' import { fromMarkdown } from 'mdast-util-from-markdown' import { gfmFromMarkdown } from 'mdast-util-gfm' import { gfm } from 'micromark-extension-gfm' import type { Nodes } from 'mdast' +/** Complete opening marker line: `` (slug captured). */ +const GENERATED_REGION_BEGIN_LINE = /^$/ +/** Complete closing marker line: `` (slug captured). */ +const GENERATED_REGION_END_LINE = /^$/ +/** Loose marker detector: any line that LOOKS like a region marker must parse as one. */ +const GENERATED_REGION_MARKER_HINT = /^