diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml index 7c05a0ada8..911d0709f1 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.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-06-18-session-surface.md -2026-06-18-session-surface.md: eeac53534c70099e4102aff9ef226702ea939654 -2026-06-18-session-surface.zh.md: c58d3da049cd6c18e564e596354f5d1831c4756f +2026-06-18-session-surface.md: 1bb3baac5e9bea3f657cbd3be2623223bda78ce2 +2026-06-18-session-surface.zh.md: bfbfd097ffeca59dec163dff911315d0ed94d028 diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.md b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md index eeac53534c..1bb3baac5e 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-session-surface.md +++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md @@ -64,9 +64,9 @@ Every surface-eligible event must carry `surfaceOp` or it would disappear from d - **`packages/core/session`**: `surface.ts` (`SurfaceManager`) maintains one ordered seq array for candidate acceptance and live projection; `SessionSurface` is its readonly public view. `SurfaceOp`/`SurfaceIntent` and the top-level session-event fields record how entries join it. `append()` requires a `SurfaceIntent` for surface events, `deriveMessages()` walks the surface as the sole derivation path, and `repair.ts` emits surface-aware closers. The seed constructor rejects a surface-eligible seed event missing its `surfaceOp` marker (see § Invariants). - **`packages/core/agent-loop`**: All surface-capable appends pass surface opts. Chunk seqs are collected for `assistant/message` provenance; `tool/call` seqs are captured for `tool/result` provenance. -- **`packages/session-persistence/session-persistence-sqlite`**: Two new nullable TEXT columns (`source_event_seqs`, `surface_op`) on the `events` table; `SCHEMA_VERSION` bumped (bump-and-reject, no migration). -- **`packages/session-persistence/session-persistence-jsonl`**: No changes required. -- **`packages/session-persistence/session-persistence`**: Abstract interface unchanged. +- **`packages/session/session-persistence-sqlite`**: Two new nullable TEXT columns (`source_event_seqs`, `surface_op`) on the `events` table; `SCHEMA_VERSION` bumped (bump-and-reject, no migration). +- **`packages/session/session-persistence-jsonl`**: No changes required. +- **`packages/session/session-persistence`**: Abstract interface unchanged. The surface is the foundation for future history manipulation. A compaction or tool-result-prune plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed entries — the new event takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically. diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md b/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md index c58d3da049..bfbfd097ff 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md @@ -64,9 +64,9 @@ export type SurfaceOp = - **`packages/core/session`**:`surface.ts`(`SurfaceManager`)维护一个用于候选接纳和实时投影的有序 seq 数组;`SessionSurface` 是其只读公共视图。`SurfaceOp`/`SurfaceIntent` 与顶层会话事件字段记录条目如何加入它。`append()` 要求 surface 事件携带 `SurfaceIntent`,`deriveMessages()` 以遍历 surface 作为唯一派生路径,`repair.ts` 则发出 surface 感知的闭合事件。种子构造函数拒绝缺少 `surfaceOp` 标记的可进入 surface 的种子事件(见「不变式」一节)。 - **`packages/core/agent-loop`**:所有涉及 surface 事件的追加操作都传入 surface 选项。收集分片 seq 用于 `assistant/message` 溯源;捕获 `tool/call` seq 用于 `tool/result` 溯源。 -- **`packages/session-persistence/session-persistence-sqlite`**:`events` 表新增两个可空 TEXT 列(`source_event_seqs`、`surface_op`);`SCHEMA_VERSION` 递增(bump-and-reject,无迁移)。 -- **`packages/session-persistence/session-persistence-jsonl`**:无需改动。 -- **`packages/session-persistence/session-persistence`**:抽象接口不变。 +- **`packages/session/session-persistence-sqlite`**:`events` 表新增两个可空 TEXT 列(`source_event_seqs`、`surface_op`);`SCHEMA_VERSION` 递增(bump-and-reject,无迁移)。 +- **`packages/session/session-persistence-jsonl`**:无需改动。 +- **`packages/session/session-persistence`**:抽象接口不变。 Surface 是未来历史操纵的基础。压缩或 tool-result-prune 插件追加一个既有的消息产出事件类型(例如一条携带摘要的 `user/message`),附带 `surfaceOp: { op: 'replace', start, end }` 和覆盖被遮蔽条目的 `sourceEventSeqs`——新事件在 surface 上取代该范围的位置,而插件自身的 trace 事件(如 `compaction/start`、`compaction/end`)不进入 surface。回放以确定性方式保留该决策。 diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml index e1171d5877..863045f2ce 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.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-06-20-generic-long-running-tool-runtime.md -2026-06-20-generic-long-running-tool-runtime.md: cb9d9487cd274696ae20dddab8c6888b4cf4b833 -2026-06-20-generic-long-running-tool-runtime.zh.md: fe5ca223975445991e7fe96376cbfccf432ad241 +2026-06-20-generic-long-running-tool-runtime.md: 8cf6a34c3e4fc042bd73c147d83838734dfc9585 +2026-06-20-generic-long-running-tool-runtime.zh.md: 5af06b78e97980822687cdc82d9b0672a42b6fc2 diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md index cb9d9487cd..8cf6a34c3e 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md @@ -23,7 +23,7 @@ Long-running tools are producers. `dsh-tool-bash` adapts a `BashProcess` into in ## Runtime contract -The literal types live in the [task data-structure catalog](../../../../docs/core-data-structures/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, optional positive `outputLimitBytes`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id. +The literal types live on the [tasks subsystem page](../../../../docs/subsystems/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, optional positive `outputLimitBytes`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id. `outputLimitBytes` is producer-owned presentation policy, not a registry buffer. The registry validates and projects it unchanged into `TaskSnapshot`; generic control surfaces apply the cap to complete model-facing output after adding their own status or notice metadata. Omitting it preserves the existing surface behavior, so the runtime does not impose a hidden default on unrelated producer families. diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md index fe5ca22397..5af06b78e9 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md @@ -23,7 +23,7 @@ Status: implemented ## 运行时契约 -字面类型见[任务数据结构目录](../../../../docs/core-data-structures/tasks.md)。生产方调用 `ctx.tasks.start()`,传入 kind、label、可选的所属 `Agent`、可选的正数 `outputLimitBytes` 与一个 `run()` 函数。运行时会在调用 `run()` 前完成所有可能失败的预检工作,并且只调用一次。`run()` 返回钩子后,注册过程不会再执行可能失败的步骤而直接提交;生产方无法启动没有可收集 task id 的工作。 +字面类型见[任务子系统页面](../../../../docs/subsystems/tasks.md)。生产方调用 `ctx.tasks.start()`,传入 kind、label、可选的所属 `Agent`、可选的正数 `outputLimitBytes` 与一个 `run()` 函数。运行时会在调用 `run()` 前完成所有可能失败的预检工作,并且只调用一次。`run()` 返回钩子后,注册过程不会再执行可能失败的步骤而直接提交;生产方无法启动没有可收集 task id 的工作。 `outputLimitBytes` 是生产方拥有的呈现策略,而非注册表缓冲区。注册表校验该值,并将其原样投影到 `TaskSnapshot`;通用控制接口添加自身的状态或通知元数据后,再将该上限应用于完整的面向模型输出。省略该值时保持现有接口行为,因此运行时不会向无关的生产方类别施加隐式默认值。 diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml index b6788a47ef..3db3ea7fa4 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.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-06-21-mandatory-app-attribution-headers.md -2026-06-21-mandatory-app-attribution-headers.md: ad9d65805c8f0c96bd811b5036310d019760627e -2026-06-21-mandatory-app-attribution-headers.zh.md: 3021c7fcca00f2e929d997625c303f9a27dbf673 +2026-06-21-mandatory-app-attribution-headers.md: de9125bc891cc62798480e2eccb2c90cb633de3a +2026-06-21-mandatory-app-attribution-headers.zh.md: cdada9cb235d57ad5cac98ebe0572013cac728ef diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md index ad9d65805c..de9125bc89 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md +++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md @@ -51,7 +51,7 @@ Endpoint detection is not part of this Agent Note because no endpoint-specific m The landed contract: -- `dsh-llm` documents the mandatory `User-Agent` attribution contract for `LlmAdapter` authors (`LlmAdapter` JSDoc, package README, and the adapter-contract section of `docs/core-data-structures/llm-streaming.md`). +- `dsh-llm` documents the mandatory `User-Agent` attribution contract for `LlmAdapter` authors (`LlmAdapter` JSDoc, package README, and the adapter-contract section of `docs/subsystems/llm-streaming.md`). - A shared helper (`attributionHeaders` / `userAgent`) constructs the app identity and the standard `User-Agent` value from package metadata, so adapters do not hand-copy version constants. - `dsh-llm-deepseek` sends the shared `User-Agent` on every request and its mock-server suite asserts the exact value. - `dsh-llm-pi-ai` sends the same `User-Agent` through pi-ai's `StreamOptions.headers` hook and its mock-server suite asserts the exact value. diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md index 3021c7fcca..cdada9cb23 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md @@ -51,7 +51,7 @@ LLM(大语言模型)提供方请求应当标识发出请求的产品。这 已落地的契约: -- `dsh-llm` 为 `LlmAdapter` 作者文档化了强制的 `User-Agent` 归属契约(`LlmAdapter` JSDoc、包 README,以及 `docs/core-data-structures/llm-streaming.md` 的适配器契约(adapter contract)章节)。 +- `dsh-llm` 为 `LlmAdapter` 作者文档化了强制的 `User-Agent` 归属契约(`LlmAdapter` JSDoc、包 README,以及 `docs/subsystems/llm-streaming.md` 的适配器契约(adapter contract)章节)。 - 共享辅助函数(`attributionHeaders` / `userAgent`)从包元数据构建应用身份和标准 `User-Agent` 值,适配器无需手动复制版本常量。 - `dsh-llm-deepseek` 在每个请求上发送共享的 `User-Agent`,其 mock 服务器套件断言精确值。 - `dsh-llm-pi-ai` 通过 pi-ai 的 `StreamOptions.headers` 钩子发送相同的 `User-Agent`,其 mock 服务器套件断言精确值。 diff --git a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.i18n.yaml index 5dbfcf94ef..459ffa188c 100644 --- a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.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-06-30-bash-stdin-env-trusted-plugin-surface.md -2026-06-30-bash-stdin-env-trusted-plugin-surface.md: 556d5dd86dfcc92c4628e68c19390f0033560d25 -2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md: 325e3d303bdb6836c8928fdae00de59fb954ff37 +2026-06-30-bash-stdin-env-trusted-plugin-surface.md: aa0c785be84bcb8a49f8a2670afa0f6bed6277db +2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md: 48108fdd5b8217263590fba336480f1b487e882f diff --git a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md index 556d5dd86d..aa0c785be8 100644 --- a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md +++ b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md @@ -30,4 +30,4 @@ Three deliberate choices: ## Consequences -Hook bridges pass JSON payloads and hook-specific variables through the existing bash seam, retaining its process-group, truncation, and spill behavior. The model surface remains unchanged, and the bash tool remains the sole owner of model-call request construction. The vocabulary lives in [the bash data-structure reference](../../../../docs/core-data-structures/bash.md). +Hook bridges pass JSON payloads and hook-specific variables through the existing bash seam, retaining its process-group, truncation, and spill behavior. The model surface remains unchanged, and the bash tool remains the sole owner of model-call request construction. The vocabulary lives in [the bash data-structure reference](../../../../docs/subsystems/bash.md). diff --git a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md index 325e3d303b..48108fdd5b 100644 --- a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md @@ -30,4 +30,4 @@ Status: implemented ## 后果 -钩子桥接层通过既有的 bash seam 传递 JSON 载荷和钩子特定变量,保留其进程组终止、截断和溢出行为。模型接口面不变,bash 工具仍是模型调用请求构建的唯一所有者。相关词汇定义见 [bash 数据结构参考](../../../../docs/core-data-structures/bash.md)。 +钩子桥接层通过既有的 bash seam 传递 JSON 载荷和钩子特定变量,保留其进程组终止、截断和溢出行为。模型接口面不变,bash 工具仍是模型调用请求构建的唯一所有者。相关词汇定义见 [bash 数据结构参考](../../../../docs/subsystems/bash.md)。 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-07-tool-call-timeout-policy.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml index f51277307f..e400692950 100644 --- a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.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-07-tool-call-timeout-policy.md -2026-07-07-tool-call-timeout-policy.md: 69fd1ee721de69621d3b57c10d960da0651b94dd -2026-07-07-tool-call-timeout-policy.zh.md: 0b8d07788dce152985e133923bb673f80175e68d +2026-07-07-tool-call-timeout-policy.md: b85a4d2d60ef60d9f75bd47d4d405aa55c132af6 +2026-07-07-tool-call-timeout-policy.zh.md: e67b9e4223f486208061f3944369cf3ec06fe72d diff --git a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md index 69fd1ee721..b85a4d2d60 100644 --- a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md +++ b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md @@ -40,7 +40,7 @@ That the catch is the base `next` — not something outside the waterfall — is ### The `timeout-policy` plugin -The plugin is `@deepseek-ai/dsh-timeout-policy`, a zero-config function/namespace plugin (`name` / `inject` / `apply`) in the `packages/timeout/` group. The per-tool budget is DECLARED on the tool, not on this plugin: a `ToolDefinition` carries an optional `timeoutMs`, which the owning tool plugin sets from its own config. `dsh-tool-web`, for example, resolves `fetchTimeoutMs` / `searchTimeoutMs` (default 30000) onto the `web_fetch` / `web_search` definitions: +The plugin is `@deepseek-ai/dsh-timeout-policy`, a zero-config function/namespace plugin (`name` / `inject` / `apply`) in the `packages/guard/` group (originally its own `timeout/` group). The per-tool budget is DECLARED on the tool, not on this plugin: a `ToolDefinition` carries an optional `timeoutMs`, which the owning tool plugin sets from its own config. `dsh-tool-web`, for example, resolves `fetchTimeoutMs` / `searchTimeoutMs` (default 30000) onto the `web_fetch` / `web_search` definitions: ```yaml - id: timeout-policy @@ -89,7 +89,7 @@ A future model-facing grep/glob tool can be implemented on top of `ctx.bash` wit ## Alternatives considered -**Name the plugin `tool-timeout`.** The literal Agent Note name matched the `gen-tool-catalog` completeness guard's `packages/*/tool-*` glob, which requires every match to register a model-facing tool. This plugin registers none — it is a `tools/execute` wrapper — so a `tool-*` name would either fail `verify-tool-catalog` or force a misleading boot entry. The package is `@deepseek-ai/dsh-timeout-policy` in a new `packages/timeout/` group; the cordis.yml `id` can still be `timeout-policy`. +**Name the plugin `tool-timeout`.** The literal Agent Note name matched the `gen-tool-catalog` completeness guard's `packages/*/tool-*` glob, which requires every match to register a model-facing tool. This plugin registers none — it is a `tools/execute` wrapper — so a `tool-*` name would either fail `verify-tool-catalog` or force a misleading boot entry. The package is `@deepseek-ai/dsh-timeout-policy` in what was then a new `timeout/` group, since folded into `packages/guard/`; the cordis.yml `id` can still be `timeout-policy`. **Keep per-tool timeout handling only.** This was the shape for `bash` and `web_fetch`, and it matches Claude Code and Codex for shell commands. It loses for web-style tools because every new timeout-capable tool must choose validation, cap semantics, docs, snapshots, and classification. The plugin centralizes policy and classification while leaving each tool's schema focused on business input. diff --git a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md index 0b8d07788d..e67b9e4223 100644 --- a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md @@ -40,7 +40,7 @@ catch 是基础 `next`(而非 waterfall 之外的东西)这一点至关重 ### `timeout-policy` 插件 -该插件是 `@deepseek-ai/dsh-timeout-policy`,一个零配置的函数/命名空间插件(`name` / `inject` / `apply`),位于 `packages/timeout/` 组。每个工具的预算声明在工具自身,而非本插件:`ToolDefinition` 携带一个可选的 `timeoutMs`,由拥有该工具的插件从自身配置中设置。例如 `dsh-tool-web` 将 `fetchTimeoutMs` / `searchTimeoutMs`(默认 30000)解析到 `web_fetch` / `web_search` 的定义上: +该插件是 `@deepseek-ai/dsh-timeout-policy`,一个零配置的函数/命名空间插件(`name` / `inject` / `apply`),位于 `packages/guard/` 组。每个工具的预算声明在工具自身,而非本插件:`ToolDefinition` 携带一个可选的 `timeoutMs`,由拥有该工具的插件从自身配置中设置。例如 `dsh-tool-web` 将 `fetchTimeoutMs` / `searchTimeoutMs`(默认 30000)解析到 `web_fetch` / `web_search` 的定义上: ```yaml - id: timeout-policy @@ -89,7 +89,7 @@ function toolTimeoutResult(timeoutMs: number): ToolExecutionResult { ## 曾考虑的替代方案 -**将插件命名为 `tool-timeout`。** 字面的 Agent Note 名称匹配了 `gen-tool-catalog` 完整性守卫的 `packages/*/tool-*` glob,该 glob 要求每个匹配项注册一个面向模型的工具。本插件不注册任何工具——它是一个 `tools/execute` 包装器——因此 `tool-*` 名称要么导致 `verify-tool-catalog` 失败,要么强制产生一个误导性的启动条目。包为 `@deepseek-ai/dsh-timeout-policy`,位于新的 `packages/timeout/` 组;cordis.yml 的 `id` 仍可为 `timeout-policy`。 +**将插件命名为 `tool-timeout`。** 字面的 Agent Note 名称匹配了 `gen-tool-catalog` 完整性守卫的 `packages/*/tool-*` glob,该 glob 要求每个匹配项注册一个面向模型的工具。本插件不注册任何工具——它是一个 `tools/execute` 包装器——因此 `tool-*` 名称要么导致 `verify-tool-catalog` 失败,要么强制产生一个误导性的启动条目。包为 `@deepseek-ai/dsh-timeout-policy`,位于新的 `packages/guard/` 组;cordis.yml 的 `id` 仍可为 `timeout-policy`。 **仅保留逐工具的超时处理。** 这是 `bash` 和 `web_fetch` 的既有形态,也与 Claude Code 和 Codex 对 shell 命令的做法一致。它对 web 类工具不利,因为每个新的支持超时的工具都必须自行选择校验方式、上限语义、文档、快照和分类。插件集中了策略和分类,让每个工具的 schema 专注于业务输入。 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/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index 7a0b74dfaf..08d8319d33 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.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-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: f3da981c478ef08672a82f23ab9cd42e0f38ebab -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 5a8e15aab79875bdb08e6347199924c4215a2705 +2026-07-10-single-file-executable-sdk-runtime-distribution.md: fe09e1b83f4f0a9f867f78351f643b1bd26d7c9d +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: ee49d7ea8fade9d3e323e8c6921479b8053809af diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index f3da981c47..fe09e1b83f 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -27,8 +27,8 @@ Terminology reminder: pkg's `/snapshot` VFS has nothing to do with this repo's t The deterministic protocol implementation (`server.ts` / `transport.ts`) lands as two packages on the existing `acp/acp` + `examples/acp-demo` pattern — the serving surface is itself a plugin: -- [`packages/ui/jsonrpc`](../../../../packages/ui/jsonrpc/README.md) (`@deepseek-ai/dsh-jsonrpc`): the pure protocol plugin; on apply it mounts `HarnessSdkServer` plus a line-delimited JSON-RPC transport on the process stdio, with disposal through `ctx.effect()`. Whether to serve is decided by `cordis.yml`; a yml that does not mount it is a legitimate process that does not serve. Protocol-level exit belongs to the plugin (after answering and flushing the `shutdown` response it disposes the root runtime so persistence drains, then `exit(0)`; an HMR-style unload only stops the service without exiting the process). -- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md) (`@deepseek-ai/dsh-jsonrpc-demo`): a thin app bin — `installFailLoud` + `loadEnv` + config discovery + `boot()` from [`dsh-app-boot`](../../../../packages/ui/app-boot/src/index.ts), done once boot completes; the server is brought up by the `dsh-jsonrpc` entry in the yml. Its only dependency is app-boot. Process-level exit belongs to the bin (stdin EOF/SIGTERM → dispose then 0, SIGINT → 130). +- [`packages/scaffold/server`](../../../../packages/scaffold/server/README.md) (`@deepseek-ai/dsh-jsonrpc`): the pure protocol plugin; on apply it mounts `HarnessSdkServer` plus a line-delimited JSON-RPC transport on the process stdio, with disposal through `ctx.effect()`. Whether to serve is decided by `cordis.yml`; a yml that does not mount it is a legitimate process that does not serve. Protocol-level exit belongs to the plugin (after answering and flushing the `shutdown` response it disposes the root runtime so persistence drains, then `exit(0)`; an HMR-style unload only stops the service without exiting the process). +- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md) (`@deepseek-ai/dsh-jsonrpc-demo`): a thin app bin — `installFailLoud` + `loadEnv` + config discovery + `boot()` from [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts), done once boot completes; the server is brought up by the `dsh-jsonrpc` entry in the yml. Its only dependency is app-boot. Process-level exit belongs to the bin (stdin EOF/SIGTERM → dispose then 0, SIGINT → 130). Config discovery has two channels and fails loudly when both are missing: the `DSH_CORDIS_CONFIG` environment variable first (the SDK client convention), then an argv positional argument; no default path and no built-in fallback whatsoever — "the plugins actually booted are decided by an external cordis.yml" is a hard semantic. diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index 5a8e15aab7..ee49d7ea8f 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -27,8 +27,8 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)(vercel/pkg 归档后 确定性协议实现(`server.ts` / `transport.ts`)按 `acp/acp` + `examples/acp-demo` 的既有模式落为两包——对外服务接口本身也是插件: -- [`packages/ui/jsonrpc`](../../../../packages/ui/jsonrpc/README.md)(`@deepseek-ai/dsh-jsonrpc`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkServer` 与按行传输的 JSON-RPC 层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并刷新 `shutdown` 响应后 dispose 根运行时以排空持久化,再调用 `exit(0)`;HMR 式卸载只停止服务,不退出进程)。 -- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md)(`@deepseek-ai/dsh-jsonrpc-demo`):轻量应用入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/ui/app-boot/src/index.ts) 的 `boot()`;`boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-jsonrpc` 条目启动。它只依赖 `app-boot`。进程级退出归 `bin` 所有(stdin EOF/SIGTERM → dispose 后返回 0,SIGINT → 130)。 +- [`packages/scaffold/server`](../../../../packages/scaffold/server/README.md)(`@deepseek-ai/dsh-jsonrpc`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkServer` 与按行传输的 JSON-RPC 层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并刷新 `shutdown` 响应后 dispose 根运行时以排空持久化,再调用 `exit(0)`;HMR 式卸载只停止服务,不退出进程)。 +- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md)(`@deepseek-ai/dsh-jsonrpc-demo`):轻量应用入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts) 的 `boot()`;`boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-jsonrpc` 条目启动。它只依赖 `app-boot`。进程级退出归 `bin` 所有(stdin EOF/SIGTERM → dispose 后返回 0,SIGINT → 130)。 配置发现有两个通道,均缺失时立即报错:优先使用 `DSH_CORDIS_CONFIG` 环境变量(SDK 客户端约定),其次使用 argv 位置参数;没有默认路径或内置回退——「实际启动的插件由外部 `cordis.yml` 决定」是硬语义。 diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml index 465a658ca8..2d39db2332 100644 --- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.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-14-provider-routed-llm-adapters.md -2026-07-14-provider-routed-llm-adapters.md: 27277280e423553f79d5a34f512b673413f495ff -2026-07-14-provider-routed-llm-adapters.zh.md: aeb09a500d5750ef2793bc9a7fc09834055a56a4 +2026-07-14-provider-routed-llm-adapters.md: 9039334370ba5d71eb71879970c2e572a5b023df +2026-07-14-provider-routed-llm-adapters.zh.md: eff33496e1a61472534009533270f8f943ccc1f8 diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md index 27277280e4..9039334370 100644 --- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md @@ -84,7 +84,7 @@ The on-disk session format remains the pre-release pinned version `0`, with no c - Unit coverage exercises registry conflicts, request reconstruction, session validation, profile resolution, single-attempt option forwarding, native API selection including OpenAI Responses, conversion, replay validation, error mapping, caller cancellation, idle-timeout transport termination, content rewrites, and same-instance versus different-instance replay dispatch. - Keyless loop/session tests and ACP snapshots exercise durable provider/model metadata, resume and fork propagation, workflow/subagent overrides, and unchanged user-visible transcripts; the key-gated DeepSeek e2e retains real provider streaming and tool follow-up coverage. -- Public JSDoc, package READMEs, architecture and core-data-structure docs, generated catalogs, examples, session fixtures, and Python SDK pairs use provider/model targets consistently and are checked by the repository documentation and type-equivalence gates. +- Public JSDoc, package READMEs, architecture and subsystem docs, generated catalogs, examples, session fixtures, and Python SDK pairs use provider/model targets consistently and are checked by the repository documentation and type-equivalence gates. ## Risks diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md index aeb09a500d..eff33496e1 100644 --- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md @@ -8,9 +8,9 @@ Status: implemented `dsh-llm` 按精确模型名称注册适配器。插件在 Cordis 启动时提供模型列表,`LlmService` 为列表中的每个字符串保存一个适配器,`GenerateOptions.model` 同时选择适配器与提供方模型。两个正式适配器都只面向相同的两个 DeepSeek 模型时,这种方式可以工作,但它混淆了两个独立决策:由哪个上游提供方承接请求,以及该提供方应运行哪个模型。 -这种混淆使提供方网关无法提供开放的模型目录。例如,OpenRouter 是一个包含大量模型 ID 的提供方,私有 OpenAI 兼容端点也可能在不修改 harness 插件树的情况下增加模型。目前,每个新选择的模型都必须在插件启动期间完成注册。同一个模型 ID 还可能存在于多个提供方中,因此仅按模型注册无法表达调用方预期使用的提供方。 +这种混淆使提供方网关无法提供开放的模型目录。例如,OpenRouter 是一个包含大量模型 ID 的提供方,私有 OpenAI 兼容端点也可能在不修改 Harness 插件树的情况下增加模型。目前,每个新选择的模型都必须在插件启动期间完成注册。同一个模型 ID 还可能存在于多个提供方中,因此仅按模型注册无法表达调用方预期使用的提供方。 -`dsh-llm-pi-ai` 没有暴露 pi-ai 的提供方抽象。它以内联方式构造 DeepSeek `openai-completions` 模型,应用 DeepSeek 专用载荷补丁,并将每条回放的助手消息标记为 DeepSeek。pi-ai 自身提供提供方/模型目录,能够选择 `openai-responses`、`anthropic-messages`、`google-generative-ai` 等 API,并保留提供方专用的响应 ID,以及后续轮次所需的推理(reasoning)和工具签名。harness 转换丢弃了这些来源信息,因此仅将内联模型替换为目录查询,会导致同模型回放与跨提供方移交不完整。 +`dsh-llm-pi-ai` 没有暴露 pi-ai 的提供方抽象。它以内联方式构造 DeepSeek `openai-completions` 模型,应用 DeepSeek 专用的 payload 补丁,并将每条回放的助手消息标记为 DeepSeek。pi-ai 自身提供提供方/模型目录,能够选择 `openai-responses`、`anthropic-messages`、`google-generative-ai` 等 API,并保留提供方专用的响应 ID,以及后续轮次所需的推理和工具签名。Harness 转换丢弃了这些来源信息,因此仅将内联模型替换为目录查询,会导致同模型回放与跨提供方移交不完整。 适配器配置同样假定只存在一个 DeepSeek API 密钥和端点。通用后端需要为各提供方分别配置凭据和端点覆盖,同时继续由 pi-ai 处理 AWS、Google ADC、OAuth 等环境认证机制。 @@ -18,9 +18,9 @@ Status: implemented ### 提供方作为适配器注册键 -`GenerateOptions` 与 `LlmCallConfig` 在 `model: string` 之外携带 `provider: string`,`AgentOptions` 则携带对应的可选创建字段。只有两个值都非空时,agent loop(智能体循环)请求才有效;两个值也都是已记录请求头的一部分。`agent/request` 可以在任意步骤返回替换后的字段组合,因此会话可以切换提供方与模型,无需改变 Cordis 插件生命周期。 +`GenerateOptions` 与 `LlmCallConfig` 在 `model: string` 之外携带 `provider: string`,`AgentOptions` 则携带对应的可选创建字段。只有两个值都非空时,agent loop(智能体循环)请求才有效;两个值也都会写入请求头日志。`agent/request` 可以在任意步骤返回替换后的字段组合,因此会话可以切换提供方与模型,无需改变 Cordis 插件生命周期。 -`LlmService` 按提供方注册和解析适配器。`registerAdapter(providers, adapter)` 在修改注册表前检查整个提供方列表,遇到重复项时以 `DUPLICATE_ADAPTER` 拒绝注册,并将整组注册作为一个 effect 统一 dispose(资源释放)。模型 ID 不作为注册键;仍由选中的适配器负责验证或转发。后续的 [LLM 目录与 ACP(Agent Client Protocol)模型选择 Agent Note](2026-07-15-llm-model-catalog-and-acp-selection.md) 增加了建议性的 `listProviders()` / `listModels()` 发现接口,但不会把目录成员关系变成请求校验规则。 +`LlmService` 按提供方注册和解析适配器。`registerAdapter(providers, adapter)` 在修改注册表前检查整个提供方列表,遇到重复项时返回 `DUPLICATE_ADAPTER`,并将整组注册作为一个 effect 释放。模型 ID 不作为注册键;仍由选中的适配器负责验证或转发。后续的 [LLM 目录与 ACP 模型选择 Agent Note](2026-07-15-llm-model-catalog-and-acp-selection.md) 增加了建议性的 `listProviders()` / `listModels()` 发现接口,但不会把目录成员关系变成请求校验规则。 在一个 Cordis 上下文中,一个提供方只能有一个适配器所有者。`dsh-llm-deepseek` 注册 `deepseek`;`dsh-llm-pi-ai` 也可以注册 `deepseek`,但同时加载两个所有者属于配置错误,不采用顺序规则或回退行为。若部署选择手写的 DeepSeek 实现,需从 pi-ai 配置中排除 `deepseek`;若部署选择 pi-ai 的 DeepSeek 实现,则不挂载 `dsh-llm-deepseek`。 @@ -28,29 +28,29 @@ Status: implemented ### 显式 pi-ai 提供方配置 -`dsh-llm-pi-ai` 接受一个非空的提供方配置列表。列表内的提供方名称必须唯一,并且存在于 pi-ai 的 `getProviders()` 结果中。每项配置包含提供方名称,以及可选的 `apiKey`、`baseURL`、headers、推理级别和预算、缓存保留设置、传输方式、SDK 超时、harness 流空闲超时,以及由提供方拥有的 `retryPolicy`。适配器强制将 pi-ai 的 `maxRetries` 设为零,使一次 `stream()` 调用只发起一次可见的提供方请求;`dsh-llm-retry` 则在 agent 失败步骤 seam 上执行解析后的策略。凭据不设全局值:显式密钥仅对所属配置生效;未提供密钥时,pi-ai 使用标准环境变量、OAuth token、AWS 凭据链、Google ADC 或其他提供方原生环境认证。显式空密钥属于无效配置,不会回退到环境认证。 +`dsh-llm-pi-ai` 接受一个非空的提供方配置列表。列表内的提供方名称必须唯一,并且存在于 pi-ai 的 `getProviders()` 结果中。每项配置包含提供方名称,以及可选的 `apiKey`、`baseURL`、headers、推理级别和预算、缓存保留设置、传输方式、SDK 超时、Harness 流空闲超时,以及由提供方拥有的 `retryPolicy`。适配器强制将 pi-ai 的 `maxRetries` 设为零,使一次 `stream()` 调用只发起一次可见的提供方请求;`dsh-llm-retry` 则在 agent 失败步骤 seam 上执行解析后的策略。凭据不设全局值:显式密钥仅对所属配置生效;未提供密钥时,pi-ai 使用标准环境变量、OAuth token、AWS 凭据链、Google ADC 或其他提供方原生环境认证。显式空密钥属于无效配置,不会回退到环境认证。 插件通过一次全有或全无调用,将所有已配置的提供方名称注册到同一个 `PiAiAdapter`。请求按 provider 选择对应配置,并在 `getModels(provider)` 中查找模型以取得目录描述符。未知提供方会在插件加载时失败;未知模型会在网络 I/O 前以 `UNKNOWN_MODEL` 失败。适配器不会修改目录对象。当配置提供 `baseURL` 时,适配器复制选中的描述符,仅覆盖 `baseUrl`,使私有端点保留 pi-ai 的 API、能力、兼容标志、上下文限制与推理映射。私有端点必须实现所选提供方的协议,模型 ID 也仍须存在于已安装的 pi-ai 目录中。 -适配器调用 pi-ai 的 `streamSimple()`,因此每个目录模型会选择其注册的 API 实现;描述符为 `openai-responses` 时使用 OpenAI Responses,而非 Chat Completions。harness 的 temperature、最大 token 数、signal、session ID,以及提供方配置中的通用流选项均直接传递。配置 headers 与 harness 强制归因 headers 合并;发生保留名称冲突时,以 harness 归因为准。适配器不再维护 DeepSeek 专用载荷重写或提供方协议矩阵。 +适配器调用 pi-ai 的 `streamSimple()`,因此每个目录模型会选择其注册的 API 实现;描述符为 `openai-responses` 时使用 OpenAI Responses,而非 Chat Completions。Harness 的 temperature、最大 token 数、signal、session ID,以及提供方配置中的通用流选项均直接传递。配置 headers 与 Harness 强制归因 headers 合并;发生保留名称冲突时,以 Harness 归因为准。适配器不再维护 DeepSeek 专用 payload 重写或提供方协议矩阵。 -pi-ai 的通用流选项不支持停止序列。若 harness `stop` 选项已定义,`dsh-llm-pi-ai` 会以 `UNSUPPORTED_OPTION` 拒绝请求,不会静默忽略,也不会增加第二套提供方专用 payload 实现。`dsh-llm-deepseek` 继续通过原生请求序列化器支持 `stop`。 +pi-ai 的通用流选项不支持停止序列。若 Harness `stop` 选项已定义,`dsh-llm-pi-ai` 会以 `UNSUPPORTED_OPTION` 拒绝请求,不会静默忽略,也不会增加第二套提供方专用 payload 实现。`dsh-llm-deepseek` 继续通过原生请求序列化器支持 `stop`。 ### 持久化助手来源信息与回放状态 助手消息携带提供方无关的来源信息,其中包含请求的 `provider` 和 `model`,以及可选的 JSON 可序列化适配器回放状态。成功的 `assistant/message` 会话事件记录这些来源信息,`deriveMessages()` 返回助手消息时也会包含这些信息。用户、system、context 与工具结果消息不携带助手来源信息。provider/model 字段是 agent loop 的权威数据;适配器仅拥有其不透明回放状态 payload。 -成功的终止 `finish` 分片可以携带回放状态,`BlockAssembler` 会将其与 token 用量和结束原因一起保留。agent loop 会把该状态附加到组装后的助手来源信息,不再暴露响应改写 hook。错误或中止响应不会生成正常助手消息,因此不会进入后续模型历史。 +成功的终止 `finish` 分片可以携带回放状态,`BlockAssembler` 会将其与 token 用量和结束原因一起保留。只有当 `agent/step-result` 处理后的内容与提供方组装输出在结构上相等时,agent loop 才会把回放状态附加到助手来源信息。监听器重写内容后,provider/model 来源信息仍会保留,但已经陈旧的回放状态会被移除。错误或中止响应不会生成正常助手消息,因此不会进入后续模型历史。 -pi-ai 回放状态是其成功 `AssistantMessage` 的带版本最小投影,包含源 API/provider/model、响应 ID/model、停止原因,以及按索引对齐的文本、thinking 和工具调用签名。它不会重复 harness 内容块中已有的文本或工具参数,也不包含诊断信息、时间戳、用量或错误。后续请求中,只有历史提供方和目标提供方当前归同一个适配器实例所有时,`LlmService` 才会把回放状态交给目标适配器。适配器在能够恢复历史响应时,将 harness 记录的内容与回放状态组合,并负责所需的跨模型或跨提供方转换。适配器收到未知版本或块形状不匹配的回放状态时会显式失败;其他适配器只能收到提供方无关的内容与来源信息。 +pi-ai 回放状态是其成功 `AssistantMessage` 的带版本最小投影,包含源 API/provider/model、响应 ID/model、停止原因,以及按索引对齐的文本、thinking 和工具调用签名。它不会重复 Harness 内容块中已有的文本或工具参数,也不包含诊断信息、时间戳、用量或错误。后续请求中,只有历史提供方和目标提供方当前归同一个适配器实例所有时,`LlmService` 才会把回放状态交给目标适配器。适配器在能够恢复历史响应时,将 Harness 记录的内容与回放状态组合,并负责所需的跨模型或跨提供方转换。适配器收到未知版本或块形状不匹配的回放状态时会显式失败;其他适配器只能收到提供方无关的内容与来源信息。 该状态属于模型可见的回放输入,因此遵循现有的[请求可重建规则](2026-07-05-reconstructable-requests.md):它同时存在于终止 `finish` 分片和驱动派生的已组装 `assistant/message` 来源信息中。恢复和 fork 会原样保留该状态。压缩(compaction)遮蔽助手消息时,也会从活动 surface 中移除其回放状态;摘要属于普通的提供方无关内容。 ### 在所有请求生产方中传播目标 -每个模型选择接口都同时携带 provider 与 model:声明式 agent、ACP 和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖,以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 `model` 之外增加 `provider`。 +每个模型选择接口都同时携带 provider 与 model:声明式 agent、ACP(Agent Client Protocol)和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖,以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 `model` 之外增加 `provider`。 -压缩配置在 `summarizationModel` 之外增加 `summarizationProvider`。两个值均为空时继承,均非空时选择显式目标;只配置其中一个会导致加载失败。继承优先使用最近一次记录的请求目标,没有时回退到 agent 创建选项。`compact/summary` 使用现有模型调用封装记录两个字段。 +压缩配置在 `summarizationModel` 之外增加 `summarizationProvider`。两个值均为空时继承,均非空时选择显式目标;只配置其中一个会导致加载失败。继承优先使用最近一次记录的请求目标,没有时回退到 agent 创建选项。`compact/summary` 使用现有模型调用 envelope 记录两个字段。 JSON-RPC 运行时显式接收 provider 与 model。仅当 `deepseek` 提供方没有注册所有者时,其便利回退才会挂载 `dsh-llm-deepseek`;其他缺失的提供方会直接失败,不会猜测适配器。 @@ -68,7 +68,7 @@ JSON-RPC 运行时显式接收 provider 与 model。仅当 `deepseek` 提供方 **每个提供方挂载一个 pi-ai 插件实例。** 独立实例可以隔离配置,但会重复插件声明,也无法实现配置注册的原子性。每个请求本就向同一个适配器提供 provider,因此经过验证的配置映射具有更小的生命周期接口。 -**接受任意内联 pi-ai 模型描述符。** 这种方式可支持目录外的私有模型 ID,但会将 pi-ai 的模型与兼容性 schema 暴露为 harness 配置,并要求适配器验证协议专用组合。当前版本通过覆盖目录模型的 `baseURL` 支持自定义端点;只有实际出现目录外部署需求后,才会另行决策是否支持自定义描述符。 +**接受任意内联 pi-ai 模型描述符。** 这种方式可支持目录外的私有模型 ID,但会将 pi-ai 的模型与兼容性 schema 暴露为 Harness 配置,并要求适配器验证协议专用组合。当前版本通过覆盖目录模型的 `baseURL` 支持自定义端点;只有实际出现目录外部署需求后,才会另行决策是否支持自定义描述符。 ## 影响 @@ -84,7 +84,7 @@ JSON-RPC 运行时显式接收 provider 与 model。仅当 `deepseek` 提供方 - 单元测试覆盖注册表冲突、请求重建、会话验证、配置解析、单次请求的选项转发、包括 OpenAI Responses 在内的原生 API 选择、转换、回放验证、错误映射、调用方取消、空闲超时导致的传输终止、内容重写,以及同一实例与不同实例间的回放分发。 - 无密钥的 agent loop/会话测试和 ACP 快照覆盖持久化 provider/model 元数据、恢复与 fork 传播、工作流/subagent 覆盖,以及不变的用户可见 transcript(文本记录);密钥门控的 DeepSeek e2e 测试保留真实提供方的流式输出与工具后续调用覆盖率。 -- 公共 JSDoc、包的 README、架构与核心数据结构文档、生成目录、示例、会话 fixture(测试前置数据)和 Python SDK 配对文档统一使用 provider/model 目标,并由仓库文档与类型等价门禁校验。 +- 公共 JSDoc、package README、架构与子系统文档、生成目录、示例、会话 fixture(测试前置数据)和 Python SDK 配对文档统一使用 provider/model 目标,并由仓库文档与类型等价门禁校验。 ## 风险 diff --git a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml index 233164d4e4..c85bb100aa 100644 --- a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.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-15-agent-initiator-scope.md -2026-07-15-agent-initiator-scope.md: 69648100e76cfc212469854188d664357fec22f1 -2026-07-15-agent-initiator-scope.zh.md: 505d198ccd2a54af1a15fc1ad6c03b27d217eca0 +2026-07-15-agent-initiator-scope.md: 2f388ae1de3dd6583686e129a03f0cd76701e18d +2026-07-15-agent-initiator-scope.zh.md: 64f70a4d1c3368d5621edb7251d0b567505affc0 diff --git a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md index 69648100e7..2f388ae1de 100644 --- a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md +++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md @@ -12,7 +12,7 @@ Deep process-local infrastructure sometimes needs a trusted initiating Agent bel ## Decision -The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the initiating Agent. It stores the exact `Agent` directly rather than introducing a one-field frame; a separate private run token records nested boundary lineage only for teardown bookkeeping and carries no identity. The [core-data catalog](../../../../docs/core-data-structures/core.md#initiating-agent) identifies the carried type. +The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the initiating Agent. It stores the exact `Agent` directly rather than introducing a one-field frame; a separate private run token records nested boundary lineage only for teardown bookkeeping and carries no identity. The [core-data catalog](../../../../docs/subsystems/core.md#initiating-agent) identifies the carried type. `currentInitiator()` reads optionally, `requireInitiator()` throws `no initiating agent is active`, and `withInitiator(agent, operation)` preserves the operation's exact synchronous value or Promise. `withoutInitiator(operation)` establishes a clearing boundary for work that must not inherit an Agent. Session remains derived as `agent.session`; turn, step, tool call, `signal`, model, `cwd`, sandbox, and authorization stay with their existing owners. diff --git a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md index 505d198ccd..64f70a4d1c 100644 --- a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md @@ -12,7 +12,7 @@ harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负 ## 决策 -必需的 `ctx.agents` 服务使用 Node `AsyncLocalStorage` 携带发起 Agent。它直接存储同一个 `Agent`,不引入只有一个字段的帧;另一个私有运行标记只记录嵌套边界的谱系,供 teardown 记账使用,不携带身份。[核心数据目录](../../../../docs/core-data-structures/core.md#initiating-agent)标明了所携带的类型。 +必需的 `ctx.agents` 服务使用 Node `AsyncLocalStorage` 携带发起 Agent。它直接存储同一个 `Agent`,不引入只有一个字段的帧;另一个私有运行标记只记录嵌套边界的谱系,供 teardown 记账使用,不携带身份。[核心数据目录](../../../../docs/subsystems/core.md#initiating-agent)标明了所携带的类型。 `currentInitiator()` 用于可选读取,`requireInitiator()` 抛出 `no initiating agent is active`,`withInitiator(agent, operation)` 保留操作返回的同步值或 Promise 本身。`withoutInitiator(operation)` 会建立清空边界,供不得继承 Agent 的工作使用。会话仍通过 `agent.session` 推导;轮次、步骤、工具调用、`signal`、模型、`cwd`、沙箱和授权继续由现有归属方管理。 diff --git a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml index 2f56298d57..82d87580f8 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.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-19-cooperative-tool-cancellation.md -2026-07-19-cooperative-tool-cancellation.md: be237f6ca9475699bb4af76896772a1a7409033d -2026-07-19-cooperative-tool-cancellation.zh.md: dbc74588931a2ae75678b0026df7a8175b0d20b6 +2026-07-19-cooperative-tool-cancellation.md: 4e82452cd6883593d4693766bea803c97fa4c977 +2026-07-19-cooperative-tool-cancellation.zh.md: 1d9fd3dcc8fa65e6f06050be749eec6ac9e996c2 diff --git a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md index be237f6ca9..4e82452cd6 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md +++ b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md @@ -46,7 +46,7 @@ This decision requires cancellation at the tool invocation seam only. Making sig ## Verification -[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) proves the required exact signal types, readonly observer and tool views, mutable-but-required around-dispatch view, and `defineTool()` inference. [`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) covers pre-aborted materialization, phase skipping, policy and wrapper races, body invocation classification, caller-signal fusion, error precedence, context retention, and quiescent drainage. [`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) and [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) cover balanced durable results for undispatched siblings. [`code-mode.spec.ts`](../../../../packages/core/tools/tests/code-mode.spec.ts) and first-party integration suites cover explicit forwarding, while [`timeout-policy.spec.ts`](../../../../packages/timeout/timeout-policy/tests/timeout-policy.spec.ts) preserves timeout ownership. +[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) proves the required exact signal types, readonly observer and tool views, mutable-but-required around-dispatch view, and `defineTool()` inference. [`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) covers pre-aborted materialization, phase skipping, policy and wrapper races, body invocation classification, caller-signal fusion, error precedence, context retention, and quiescent drainage. [`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) and [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) cover balanced durable results for undispatched siblings. [`code-mode.spec.ts`](../../../../packages/core/tools/tests/code-mode.spec.ts) and first-party integration suites cover explicit forwarding, while [`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) preserves timeout ownership. No registry test can prove that arbitrary third-party same-process code observes the signal or stops in bounded time. Capability tests continue to prove cancellation and quiescence at the boundary that owns each side effect. diff --git a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md index dbc7458893..1d9fd3dcc8 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md @@ -46,7 +46,7 @@ Status: implemented ## 验证 -[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) 证明必填的精确信号类型、观察者与工具的只读视图、环绕调度可替换但不可删除的视图,以及 `defineTool()` 推断。[`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) 覆盖进入时已中止的物化与阶段跳过、策略和包装层竞态、工具主体调用分类、调用方信号融合、错误优先级、上下文保留和完全停稳。[`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) 与 [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) 覆盖为未调度的同批调用补齐持久化结果。[`code-mode.spec.ts`](../../../../packages/core/tools/tests/code-mode.spec.ts) 和第一方集成测试覆盖显式转发,[`timeout-policy.spec.ts`](../../../../packages/timeout/timeout-policy/tests/timeout-policy.spec.ts) 保持超时归属。 +[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) 证明必填的精确信号类型、观察者与工具的只读视图、环绕调度可替换但不可删除的视图,以及 `defineTool()` 推断。[`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) 覆盖进入时已中止的物化与阶段跳过、策略和包装层竞态、工具主体调用分类、调用方信号融合、错误优先级、上下文保留和完全停稳。[`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) 与 [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) 覆盖为未调度的同批调用补齐持久化结果。[`code-mode.spec.ts`](../../../../packages/core/tools/tests/code-mode.spec.ts) 和第一方集成测试覆盖显式转发,[`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) 保持超时归属。 任何注册表测试都无法证明任意第三方同进程代码会观察信号或在有界时间内停止。各能力的测试仍需在拥有相应副作用的边界证明取消与完全停稳。 diff --git a/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.i18n.yaml new file mode 100644 index 0000000000..e597017076 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.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/architecture/2026-07-29-package-regrouping.md +2026-07-29-package-regrouping.md: bb9f7deb775a0922de8b20f766446ebf007718d3 +2026-07-29-package-regrouping.zh.md: 1d7d79e0cfad95382c036b3c99712775870aa48a diff --git a/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md new file mode 100644 index 0000000000..bb9f7deb77 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md @@ -0,0 +1,96 @@ +# Agent Note: Regroup packages/ by measured clustering + +Status: implemented + +English | [中文](2026-07-29-package-regrouping.zh.md) + +## Problem + +The two-level `packages//` hierarchy ([original decision](../../archived/architecture/2026-06-20-package-hierarchy.md)) had drifted since June: 167 packages sat in 42 groups, and several group boundaries no longer matched how the packages actually cluster. + +- `ui/` mixed four unrelated planes: the human terminal channel (`tui`), the SDK's JSON-RPC server half (`jsonrpc`, whose peer dependency on `dsh-sdk-protocol` binds it to the SDK wire stack), the human-interaction seams (`user-interaction`, `user-approval`, `permission`, `tool-ask-user`, `commands`), and channel-neutral boot glue (`app-boot`). Its own README narrated the mixture instead of stating a role. +- The session family was fragmented across five groups — `session-persistence/`, `session-projection/`, `session-query/`, `session-title/`, and `telemetry/` — although the measured dependency edges tie them together (query → persistence, title → projection, projection → persistence; see [docs/module-graph.md](../../../../docs/module-graph.md)). +- Two group names collided with unrelated packages: `telemetry/` (session reporting) vs `dsh-telemetry` (launcher-side SDK telemetry), and `timeout/` (a tool-call guard) vs `util/timeout` (the generic promise utility). +- `cordis/` named its group after the framework every package is built on, so the name discriminated nothing; its single package `tool-cordis` is the runtime self-modification toolset. +- The old `sdk/` folder names were inconsistent: `sdk/sdk-client` and `sdk/sdk-protocol` repeated the group name while `sdk/telemetry`, `sdk/helper`, and `sdk/scripts` did not. + +The north star for the regrouping: **closely clustered packages share a group.** A cluster is measured — peer-dependency edges and co-change — not thematic. An isolated seam family may stand alone as a small group; the failure mode to avoid is the grab-bag whose name describes no single role. + +## Decision + +Six groups are recomposed; every other group keeps its prior boundary and contents (the dependency analysis confirmed the capability families — `bash/`, `pty/`, `code-runtime/`, `sandbox/`, `subprocess/`, `fs/`, `lsp/`, `web/`, `skill/`, and the rest — were already drawn correctly). npm package names did not change; the folder tree carries the whole change. + +| Group | Members (folder names) | From | +|---|---|---| +| `session/` | session-persistence, session-persistence-jsonl, session-persistence-sqlite, session-checkpoint-policy, session-projection, session-projection-cache, session-title, session-title-llm, session-title-first-message-llm, session-title-all-messages-llm, session-telemetry, session-telemetry-otel | `session-persistence/` + `session-projection/` + `session-title/` + `telemetry/` | +| `interaction/` | user-interaction, user-approval, permission, tool-ask-user, commands, tui | `ui/` | +| `boot/` | app-boot | `ui/` | +| `scaffold/` | helper, scripts, create-sdk, protocol, client, server, telemetry | `sdk/` + `ui/jsonrpc` | +| `guard/` | repeat-tool-guard, timeout-policy | `guard/` + `timeout/` | +| `self-modification/` | tool-cordis | `cordis/` | + +- **`session/`** is the durable session data plane: the persistence seam with its backends and checkpoint policy, the projection fold that serves whole values from that log, log-backed titles, and OTel reporting. The title fold is itself load-bearing for the read side (`session-query` peer-depends on `dsh-session-title`), so titles belong with the data plane, not in a derived-services annex. The plain name is deliberate (review: prefer names a human would say); the nearby `core/session` package remains the live in-memory service, while this group is the durable family around it. `session-query/` stays a standalone group — the read/tool surface has its own model tools and SQLite FTS backend and is consumed independently of persistence internals. Absorbing `telemetry/` ended the group-name collision with `dsh-telemetry`. +- **`interaction/`** is the human-collaboration plane plus the terminal channel that answers it: the question/approval seams, the permission preset, the model-facing `ask_user_question` tool, the human-command registry (`plan-mode` and `command-goal` already consume `commands` together with the interaction seams), and `tui` — the interactive channel is the plane's richest provider and consumer (peer edges to `commands` and `user-interaction`), and a one-package `tui/` group would spend a top-level name on one plugin. +- **`boot/`** is a role-complete single-package group: the shared bin boot glue that belongs to no channel and no assembly (consumed by `apps/cli`, the `scaffold/` launcher, and the `examples/` demo bins). +- **`scaffold/`** is the developer-tooling family: project helper, launcher, initializer, wire protocol with both ends (`server` is the former `ui/jsonrpc`), and launcher telemetry. Renamed from `sdk/` in review: the whole `packages/` tree *is* the SDK, so a group named `sdk/` inside it said nothing; `scaffold/` names the create/launch/drive-a-project role. Folders drop the legacy `sdk-` prefix (`protocol`, `client`, `server`), matching the `client/`/`host/` role-named folder style; the three affected npm names are mapped explicitly beside the group wildcard in `tsconfig.base.json` until the deferred renames land. +- **`guard/`** keeps its documented role, loop-hygiene guards, and gains the tool-call timeout enforcer, dissolving the one-package `timeout/` group whose name collided with `util/timeout`. +- **`self-modification/`** names the role `cordis/` obscured: the toolset with which the agent inspects and mounts plugins in its own live runtime, and the landing zone for future self-modification packages. + +42 groups became 39; the win is clustering correctness and truthful names, not count. + +## Deferred renames (FIXME markers) + +Five npm names should eventually change, but renaming inside the reorganization would have turned a pure-move PR into an import-churn PR. Instead, each affected package's module JSDoc carries a `FIXME` naming the intended new name. `FIXME` blocks a tagged release ([marker semantics](../../../../docs/development.md)), which is the wanted forcing function: these renames are only free while nothing external consumes the packages. + +| Current npm name | Intended name | Why | +|---|---|---| +| `@deepseek-ai/dsh-jsonrpc` | `@deepseek-ai/dsh-sdk-server` | Names the wire encoding, not the role; it is the server half of the SDK protocol | +| `@deepseek-ai/dsh-telemetry` | `@deepseek-ai/dsh-sdk-telemetry` | Collides with the `dsh-session-telemetry` family; it is launcher-side SDK telemetry | +| `@deepseek-ai/dsh-helper` | `@deepseek-ai/dsh-sdk-helper` | Indefensibly generic as a published name | +| `@deepseek-ai/dsh-scripts` | `@deepseek-ai/dsh-sdk-scripts` | Same | +| `@deepseek-ai/dsh-timeout-policy` | `@deepseek-ai/dsh-timeout-guard` | Suggestion, not settled: aligns the name with its `guard/` home; decide at resolution time | + +The first four are settled intent; resolving them converges the SDK wire stack's npm names on `dsh-sdk-*` (the npm prefix names the product stack; the `scaffold/` folder names the role). `@deepseek-ai/create-sdk` keeps its documented npm-initializer exception. + +## What the move touched + +The moves landed as `git mv` commits, one per region (the session fold; `ui/` dissolution + `scaffold/`; `guard/` merge + `self-modification/`), so rename detection carries review. A group move touched: the moved package's `tsconfig.json` relative `references` and every dependent's entry (including the `apps/cli` project references), the tsconfig aggregate and path maps, group READMEs (five new bilingual triplets, deletions for dissolved groups, the [packages/README.md](../../../../packages/README.md) hierarchy table, the root `AGENTS.md` layout map), regenerated artifacts (`docs/module-graph.md`, path-embedding catalogs, the lockfile's importer keys), and root-relative `packages/...` citations in prose and gate scripts. Remaining group-path referents (workspace configs, test globs, lint keys) were found mechanically by the acceptance gates failing loud — the repository's own misconfiguration rule. + +A group move did not touch: npm names, imports, `cordis.yml` configs, snapshot fixtures, the `pnpm-workspace.yaml`/`tsdown` globs (both `packages/*/*`), or the Python runtime manifest — all reference packages by npm name. Each FIXME landed in the same commit as the move that created its context. + +`client/` and `host/` were out of scope and are unchanged. The `experimental/` group proposal (PR #844) is orthogonal — a release-boundary container, not a clustering decision; the only interaction is a trivial merge in the packages/README.md table. + +## Alternatives considered + +**Coarse domain buckets** (`exec/` = subprocess+sandbox+bash+pty+code-runtime, `workspace/` = fs+lsp+workspace, `orchestration/` = subagent+workflow+tasks, `knowledge/` = web+skill, `collab/` = plan+todo+goal; ~16 groups). Rejected: the measured graph contradicts the merges. `sandbox` and `subprocess` are shared infrastructure consumed across families (bash ×5, fs ×5, pty, lsp, mcp, subagent, scaffold edges), `web` ↔ `skill` have zero edges, and a large bucket reproduces the `ui/` grab-bag at a larger scale. + +**Abstract layer names** (`capability/`, `policy/`, `extension/`, `provider/`). Rejected: they describe every plugin equally badly, and a `capability/` bucket would hold ~50 packages. + +**A full npm rename sweep** (`dsh--` for every package). Rejected: npm names are flat, so group-prefixing adds churn across imports, configs, and fixtures with no disambiguation gain; targeted FIXME-tracked renames cover the actual collisions. + +**Performing the five renames inside the reorganization.** Rejected in review: renames multiply open-PR conflicts and destroy the pure-move review property. The FIXME markers keep them visible release blockers to resolve as small follow-up PRs. + +**A two-way session split** (`session-core/` + `session-utils/`). Rejected: query belongs to neither side cleanly, and `session-core` invites confusion with `core/session` (`dsh-session`, the live in-memory service, which stays in `core/`). + +**A three-way session split** (`session-store/` + `session-query/` + `session-utils/`), this RFC's first draft. Rejected in review: `session-utils/` was a negatively-defined annex ("derived, nothing load-bearing depends on it") — the grab-bag shape the north star forbids, and factually wrong besides (`session-query` peer-depends on `dsh-session-title`). The invented compound names also read machine-generated; one plain `session/` group says what a human would say. Query stays standalone in both drafts: it is an independently consumed read surface with its own tool package and backend. + +**Recomposing `ui/` as a single `channels/` group** (tui + jsonrpc + acp + interaction seams + boot). Rejected: the same grab-bag under a new name — those packages serve four planes, `jsonrpc`'s measured cluster is the SDK wire stack, and `acp/` is an automation transport, not a human channel. + +**A standalone one-package `tui/` group**, this RFC's first draft. Rejected in review: `tui` is the interaction plane's primary provider/consumer (peer edges to `commands`, `user-interaction`), and a top-level name spent on one plugin adds a group without adding information; it folds into `interaction/`. + +**Keeping the group name `sdk/`.** Rejected in review: the whole `packages/` tree is the SDK, so an `sdk/` group inside it discriminates nothing — the same disease as `cordis/`. `scaffold/` names the actual role (create, launch, and drive projects from outside). + +**Moving `app-boot` to `apps/`** (review question). Rejected: `apps/` is the assembly tier over the package tier, and `dsh-app-boot` is a library that package-tier code imports (`scaffold/scripts`' launcher peer-depends on it) — placing it in `apps/` would invert the tiers and put a workspace library outside the `packages/*/*` build globs. It stays a package; `boot/` is its role-complete home. + +**Moving `tool-cordis` into `core/`.** Rejected: self-modification is its own product seam, expected to grow; the spine stays minimal. The group was first named `self-evolve/`; review settled on `self-modification/` as the plainer term. + +**Renaming `context/` to `request-context/`.** Rejected: within this tree the group is unambiguous in situ; the churn is unjustified. + +## Consequences + +- The tree matches the map: the six recomposed groups hold exactly the listed members; the groups `ui/`, `sdk/`, `telemetry/`, `timeout/`, `cordis/`, `session-persistence/`, `session-projection/`, and `session-title/` no longer exist; every other group's contents are unchanged. The workspace package-name set is identical before and after (zero npm renames), and the five FIXME markers pin the deferred ones. A FIXME that later proves wrong must be removed explicitly with rationale, never silently dropped. +- What pins the result: `pnpm run typecheck`, the unit suites of every moved group, `verify-package-paths`, `verify-md-links`, and the corpus-wide translation pairing all pass on the moved tree; the group-scoped test globs in `vitest.snapshot.config.ts` were rewritten with the moves so the suites collect the same test files as before (a fail-open glob would silently drop coverage). +- Every open PR touching a moved file rebases across the move once; the mapping table in the PR body and rename detection resolve most hunks mechanically. +- Single-package groups remain (`boot/`, `self-modification/`, and existing ones such as `acp/`). Accepted deliberately: each is role-complete rather than a fragment of a family, and a truthful small group beats a nominal merge. +- The `scaffold/` folders diverge from their npm names until the deferred renames land — the one transitional asymmetry, carried by three explicit `paths` entries in `tsconfig.base.json` and resolved by the FIXME renames. +- What this gave up: nothing functional — the change is navigational. Muscle memory and external links to old GitHub paths break, which is acceptable pre-release with no external consumers. diff --git a/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.zh.md b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.zh.md new file mode 100644 index 0000000000..1d7d79e0cf --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.zh.md @@ -0,0 +1,96 @@ +# Agent Note: 按实测聚类重新划分 packages/ 分组 + +Status: implemented + +[English](2026-07-29-package-regrouping.md) | 中文 + +## Problem + +两级 `packages//` 层级结构([原始决策](../../archived/architecture/2026-06-20-package-hierarchy.md))自 6 月以来已经漂移:167 个包(package)彼时坐落在 42 个组里,若干组边界已经对不上这些包的实际聚类。 + +- `ui/` 混杂了四个互不相关的平面:人类终端通道(`tui`)、SDK 的 JSON-RPC 服务端一半(`jsonrpc`,它对 `dsh-sdk-protocol` 的对等依赖(peer dependency)把它绑在 SDK 通信栈上)、人机交互 seam(`user-interaction`、`user-approval`、`permission`、`tool-ask-user`、`commands`),以及与通道无关的 boot 胶水(`app-boot`)。它自己的 README 只能逐一叙述这堆混杂,说不出一个统一职责。 +- 会话家族被割裂在五个组里——`session-persistence/`、`session-projection/`、`session-query/`、`session-title/` 与 `telemetry/`——而实测依赖边明明把它们连成一体(query → persistence、title → projection、projection → persistence;见 [docs/module-graph.md](../../../../docs/module-graph.md))。 +- 两个组名与不相干的包撞名:`telemetry/`(会话上报)撞上 `dsh-telemetry`(启动器侧 SDK telemetry),`timeout/`(一个工具调用守卫)撞上 `util/timeout`(通用 promise 工具)。 +- `cordis/` 拿所有包共同依托的框架给自己的组命名,这个名字因此毫无区分度;组里唯一的包 `tool-cordis` 是运行时自我修改工具集。 +- 旧 `sdk/` 的目录命名不一致:`sdk/sdk-client` 和 `sdk/sdk-protocol` 重复了组名,而 `sdk/telemetry`、`sdk/helper`、`sdk/scripts` 没有。 + +这次重新分组的指导准则:**聚类紧密的包同处一组。**聚类以实测为准(对等依赖边与 co-change),而非按主题归类。孤立的 seam 家族可以自成一个小组;要避免的失败形态,是名字概括不出单一职责的大杂烩组。 + +## Decision + +重组六个组;其余每个组都保持先前的边界与内容不变(依赖分析确认各能力家族——`bash/`、`pty/`、`code-runtime/`、`sandbox/`、`subprocess/`、`fs/`、`lsp/`、`web/`、`skill/` 及其余——本来就划得正确)。npm 包名一个未改;整个变更全部由目录树承载。 + +| 组 | 成员(目录名) | 来源 | +|---|---|---| +| `session/` | session-persistence、session-persistence-jsonl、session-persistence-sqlite、session-checkpoint-policy、session-projection、session-projection-cache、session-title、session-title-llm、session-title-first-message-llm、session-title-all-messages-llm、session-telemetry、session-telemetry-otel | `session-persistence/` + `session-projection/` + `session-title/` + `telemetry/` | +| `interaction/` | user-interaction、user-approval、permission、tool-ask-user、commands、tui | `ui/` | +| `boot/` | app-boot | `ui/` | +| `scaffold/` | helper、scripts、create-sdk、protocol、client、server、telemetry | `sdk/` + `ui/jsonrpc` | +| `guard/` | repeat-tool-guard、timeout-policy | `guard/` + `timeout/` | +| `self-modification/` | tool-cordis | `cordis/` | + +- **`session/`** 是持久会话数据平面:持久化 seam 连同其各后端与检查点策略、从该日志折叠(fold)出全量值对外供值的投影、日志兜底的标题,以及 OTel 上报。标题折叠本身就是读取侧的承重构件(`session-query` 对 `dsh-session-title` 声明对等依赖),所以标题属于数据平面,而非某个「派生服务」附属区。用这个朴素的名字是有意为之(评审意见:名字要像人起的);旁边的 `core/session` 包仍是常驻内存的实时服务,本组则是围绕它的持久家族。`session-query/` 保持独立成组:这个读取/工具面自带模型工具和 SQLite FTS 后端,其消费不依赖持久化内部实现。吸收 `telemetry/` 之后,与 `dsh-telemetry` 的组名冲突就此终结。 +- **`interaction/`** 是人机协作平面加上应答它的终端通道:提问/批准 seam、权限预设、面向模型的 `ask_user_question` 工具、人类命令注册表(`plan-mode` 与 `command-goal` 已经把 `commands` 和各交互 seam 放在一起消费),以及 `tui`——这个交互通道是该平面最重的提供方与消费方(对 `commands` 与 `user-interaction` 均有对等依赖边),而一个单包 `tui/` 组会把一个顶层名字花在一个插件上。 +- **`boot/`** 是角色完备的单包组:不归属任何通道也不归属任何组装的共享 bin boot 胶水(被 `apps/cli`、`scaffold/` 的启动器和 `examples/` 各演示 bin 消费)。 +- **`scaffold/`** 是开发者工具家族:项目 helper、启动器、初始化器、连同两端的通信协议(`server` 即原先的 `ui/jsonrpc`),以及启动器侧 telemetry。评审中从 `sdk/` 改名:整个 `packages/` 树本身就是 SDK,树里再放一个叫 `sdk/` 的组等于什么都没说;`scaffold/` 说出了「创建/启动/驱动项目」这一实际角色。目录去掉遗留的 `sdk-` 前缀(`protocol`、`client`、`server`),与 `client/`/`host/` 的角色命名风格一致;在推迟的改名落地之前,受影响的三个 npm 名在 `tsconfig.base.json` 里于组通配符旁显式映射。 +- **`guard/`** 保留其文档记载的角色(循环卫生守卫),并新纳入强制执行工具调用超时的包;那个与 `util/timeout` 撞名的单包组 `timeout/` 随之解散。 +- **`self-modification/`** 把 `cordis/` 遮蔽掉的角色说了出来:它是 agent(智能体)检查并挂载自身实时运行时中插件所用的工具集,也是未来自我修改类包的落点。 + +42 个组变为 39 个;收益在聚类正确与名实相符,不在数量增减。 + +## Deferred renames (FIXME markers) + +五个 npm 名最终应当改掉,但在这次重组内部改名,会把一个纯移动的 PR(Pull Request)变成大量翻改 import 的 PR。因此每个受影响包的模块 JSDoc 里带有一条 `FIXME`,写明意图中的新名字。`FIXME` 会阻塞打 tag 的发布([标记语义](../../../../docs/development.md)),这正是想要的倒逼机制:只有趁还没有外部消费方使用这些包时,这些改名才是零成本的。 + +| 当前 npm 名 | 目标名 | 原因 | +|---|---|---| +| `@deepseek-ai/dsh-jsonrpc` | `@deepseek-ai/dsh-sdk-server` | 名字说的是协议编码而非角色;它是 SDK 协议的服务端一半 | +| `@deepseek-ai/dsh-telemetry` | `@deepseek-ai/dsh-sdk-telemetry` | 与 `dsh-session-telemetry` 家族撞名;它是启动器侧 SDK telemetry | +| `@deepseek-ai/dsh-helper` | `@deepseek-ai/dsh-sdk-helper` | 作为公开发布名空泛得站不住脚 | +| `@deepseek-ai/dsh-scripts` | `@deepseek-ai/dsh-sdk-scripts` | 同上 | +| `@deepseek-ai/dsh-timeout-policy` | `@deepseek-ai/dsh-timeout-guard` | 仅为建议、尚未定案:使名字与其 `guard/` 归属对齐;到解决时再定 | + +前四个是已定的意图;兑现之后,SDK 通信栈的 npm 名随之收敛为 `dsh-sdk-*`(npm 前缀指产品栈,`scaffold/` 目录名指角色)。`@deepseek-ai/create-sdk` 保留其文档记载的 npm 初始化器特例。 + +## What the move touched + +移动以 `git mv` 提交落地,每个区域一个提交(会话折叠;`ui/` 解散加 `scaffold/`;`guard/` 合并加 `self-modification/`),评审由重命名检测承载。组移动触及了:被移动包的 `tsconfig.json` 相对 `references` 及每个依赖方的对应条目(含 `apps/cli` 的 project references);tsconfig 聚合与路径映射;各组 README(五组新的双语三文件配对、被解散组的 README 删除、[packages/README.md](../../../../packages/README.md) 的层级结构表、根 `AGENTS.md` 的布局图);重新生成的产物(`docs/module-graph.md`、内嵌路径的目录、锁文件的 importer 键);以及散文与门禁脚本中以仓库根为基准的 `packages/...` 引用。其余每一处组路径引用(workspace 配置、测试 glob、lint 键)都由验收门禁的响亮失败机械地找了出来——这正是本仓库自己的「配置错误必须响亮失败」规则。 + +组移动未触及:npm 包名、import、`cordis.yml` 配置、快照 fixture(测试前置数据)、`pnpm-workspace.yaml` 与 `tsdown` 的 glob(都是 `packages/*/*`),以及 Python 运行时 manifest(元数据清单)——它们全部按 npm 包名引用包。每条 FIXME 都与创造其上下文的那次移动落在同一个提交。 + +`client/` 与 `host/` 不在本次范围内,保持不变。`experimental/` 组提案(PR #844)与本案正交:它是发布边界容器,不是聚类决策;两者唯一的交集是 packages/README.md 表格里一次很小的合并。 + +## Alternatives considered + +**粗粒度领域桶**(`exec/` = subprocess+sandbox+bash+pty+code-runtime,`workspace/` = fs+lsp+workspace,`orchestration/` = subagent+workflow+tasks,`knowledge/` = web+skill,`collab/` = plan+todo+goal;约 16 个组)。不予采纳:实测依赖图与这些合并相矛盾。`sandbox` 和 `subprocess` 是被各家族跨界消费的共享基础设施(与 bash ×5、fs ×5、pty、lsp、mcp、subagent、scaffold 均有依赖边),`web` ↔ `skill` 之间零依赖边,而大桶只会在更大尺度上复现 `ui/` 式大杂烩。 + +**抽象分层名**(`capability/`、`policy/`、`extension/`、`provider/`)。不予采纳:这些名字对每个插件都同样地不达意,而且一个 `capability/` 桶会装下约 50 个包。 + +**一轮全量 npm 重命名**(每个包都改为 `dsh--`)。不予采纳:npm 包名是扁平的,加组前缀只会在 import、配置和 fixture 之间制造改动,却换不来任何消歧收益;用 FIXME 跟踪的定点改名足以覆盖真正的撞名。 + +**在重组内部一并完成那五个改名。** 评审中否决:改名会成倍放大开放 PR 的冲突,并破坏纯移动的评审属性。FIXME 标记让这些改名保持为可见的发布阻塞项,留待以小型后续 PR 逐一解决。 + +**会话两分法**(`session-core/` + `session-utils/`)。不予采纳:query 放哪一侧都不干净,而且 `session-core` 容易与 `core/session` 混淆(后者是 `dsh-session`,常驻内存的实时服务,留在 `core/` 不动)。 + +**会话三分法**(`session-store/` + `session-query/` + `session-utils/`),即本 RFC 的初稿。评审中否决:`session-utils/` 是靠否定条件圈出来的附属区(「派生的、没有承重方依赖」)——正是指导准则禁止的大杂烩形态,而且事实层面也站不住(`session-query` 对 `dsh-session-title` 声明对等依赖)。杜撰的复合名也读起来不像人起的;一个朴素的 `session/` 组说的就是人会说的话。query 在两版草稿中都保持独立:它是被独立消费的读取面,自带自己的工具包与后端。 + +**把 `ui/` 重组为单一 `channels/` 组**(tui + jsonrpc + acp + 交互 seam + boot)。不予采纳:不过是换个名字的同一个大杂烩——这些包服务于四个平面,`jsonrpc` 的实测聚类归属是 SDK 通信栈,而 `acp/` 是自动化传输通道,不是人类通道。 + +**独立的单包 `tui/` 组**,即本 RFC 的初稿。评审中否决:`tui` 是交互平面最重的提供方/消费方(对 `commands`、`user-interaction` 有对等依赖边),把一个顶层名字花在一个插件上只添组不添信息;它折入 `interaction/`。 + +**保留组名 `sdk/`。** 评审中否决:整个 `packages/` 树本身就是 SDK,树里的 `sdk/` 组毫无区分度——与 `cordis/` 同病。`scaffold/` 说出了实际角色(从外部创建、启动、驱动项目)。 + +**把 `app-boot` 挪到 `apps/`**(评审提问)。不予采纳:`apps/` 是包层之上的组装层,而 `dsh-app-boot` 是被包层代码 import 的库(`scaffold/scripts` 的启动器对它声明对等依赖)——放进 `apps/` 会颠倒层级,并把一个 workspace 库放到 `packages/*/*` 构建 glob 之外。它仍是一个包;`boot/` 是它角色完备的家。 + +**把 `tool-cordis` 挪进 `core/`。** 不予采纳:自我修改是独立的产品 seam,预期还会生长;主干保持精简。该组最初命名为 `self-evolve/`;评审定为更朴素的 `self-modification/`。 + +**把 `context/` 改名为 `request-context/`。** 不予采纳:在这棵树里,该组就地看并无歧义;这份改动开销并不值得。 + +## Consequences + +- 目录树与映射表一致:重组的六个组恰好持有所列成员;`ui/`、`sdk/`、`telemetry/`、`timeout/`、`cordis/`、`session-persistence/`、`session-projection/`、`session-title/` 这些组不复存在;其余每个组的内容不变。workspace 的包名集合在前后完全相同(npm 改名为零),五条 FIXME 标记钉住推迟的改名。日后若某条 FIXME 被证明不对,必须连同理由显式移除,绝不允许无声消失。 +- 结果由以下检查钉住:`pnpm run typecheck`、每个被移动组的单元测试套件、`verify-package-paths`、`verify-md-links` 与全语料翻译配对在移动后的树上全部通过;`vitest.snapshot.config.ts` 中按组划定的测试 glob 随移动一并改写,套件收集到与移动前相同的测试文件(glob 匹配为空会无声地丢失覆盖)。 +- 每个触碰被移动文件的开放 PR 都跨过这次移动做一次变基;PR 正文中的映射表与重命名检测可机械化解决大多数改动块。 +- 单包组依然存在(`boot/`、`self-modification/`,以及 `acp/` 等既有单包组)。这是有意接受的:每个都是角色完备的整体而非某个家族的碎片,一个名实相符的小组胜过一次徒有其名的合并。 +- 在推迟的改名落地之前,`scaffold/` 的目录名与其 npm 名并不一致——这是唯一的过渡性不对称,由 `tsconfig.base.json` 里三条显式 `paths` 映射承载,并由 FIXME 改名最终消除。 +- **这次变更放弃了什么:** 功能上一无所失——变更只关乎导航。肌肉记忆和指向旧 GitHub 路径的外部链接会失效;在 pre-release、尚无外部消费者的前提下,这可以接受。 diff --git a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.i18n.yaml index 6c888c7cad..e556108299 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.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-30-package-manager-native-repository-cache.md -2026-07-30-package-manager-native-repository-cache.md: f8a6706065a936ca4a9abf2a50d266a60f09b252 -2026-07-30-package-manager-native-repository-cache.zh.md: b1fea3d655f8d7aeb466744dc27bbf4ba69993ec +2026-07-30-package-manager-native-repository-cache.md: 0ea145f20a58e4e3e9feb7ba5574818c0352e2c4 +2026-07-30-package-manager-native-repository-cache.zh.md: ac72e5dc4ffa764e1dc430764a6f52fc083d32a4 diff --git a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.md b/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.md index f8a6706065..0ea145f20a 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.md +++ b/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.md @@ -44,4 +44,4 @@ An identical specifier permanently reuses its published entry. The caller change ## Testing -`packages/ui/app-boot/tests/repository-cache.spec.ts` covers same-process single-flight, cross-instance cache reuse, exact-specifier separation, failed-stage cleanup and retry, and boundary validation. Its real local-Git case invokes the bundled pnpm, runs the fixture repository's `prepare` script, and reads the prepared file from the installed cache entry without network access. +`packages/boot/app-boot/tests/repository-cache.spec.ts` covers same-process single-flight, cross-instance cache reuse, exact-specifier separation, failed-stage cleanup and retry, and boundary validation. Its real local-Git case invokes the bundled pnpm, runs the fixture repository's `prepare` script, and reads the prepared file from the installed cache entry without network access. diff --git a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.zh.md b/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.zh.md index b1fea3d655..ac72e5dc4f 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.zh.md @@ -44,4 +44,4 @@ Loader 将 `pnpm@11.7.0` 作为固定版本的运行时依赖,并使用当前 ## 测试 -`packages/ui/app-boot/tests/repository-cache.spec.ts` 覆盖同进程 single-flight、跨实例缓存复用、精确 specifier 隔离、失败暂存清理与重试,以及边界校验。其真实本地 Git 用例会调用随附的 pnpm,运行 fixture(测试前置数据)仓库的 `prepare` 脚本,并在不访问网络的情况下,从已安装缓存项中读取准备后的文件。 +`packages/boot/app-boot/tests/repository-cache.spec.ts` 覆盖同进程 single-flight、跨实例缓存复用、精确 specifier 隔离、失败暂存清理与重试,以及边界校验。其真实本地 Git 用例会调用随附的 pnpm,运行 fixture(测试前置数据)仓库的 `prepare` 脚本,并在不访问网络的情况下,从已安装缓存项中读取准备后的文件。 diff --git a/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.i18n.yaml index ba4444166c..e083adcca5 100644 --- a/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.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-08-04-credentials-yaml-and-user-environment-layer.md -2026-08-04-credentials-yaml-and-user-environment-layer.md: 4ecbc41adf4e22c74ecf425c2caf628efdf7cf54 -2026-08-04-credentials-yaml-and-user-environment-layer.zh.md: 370179b442783f4f8ecd8e3badbd236a924f5f81 +2026-08-04-credentials-yaml-and-user-environment-layer.md: 44bdea243671093b34a6338ed1c95bf25bcc78f4 +2026-08-04-credentials-yaml-and-user-environment-layer.zh.md: 5f724c7a12365e6e87e03b5926f837a5251b6118 diff --git a/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.md b/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.md index 4ecbc41adf..44bdea2436 100644 --- a/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.md +++ b/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.md @@ -23,7 +23,7 @@ OPENAI_API_KEY: sk-… Because the document holds credentials and nothing else, every deviation is a rejection rather than a skipped entry: a non-mapping root, a key that is not a POSIX identifier, a non-string value, an empty string, a duplicate key, and malformed YAML all fail — loud at boot and at a write, warn-and-keep-the-last-good-snapshot on a live reload. A silently ignored key would read as "the secret I stored has no effect", which is the failure this change exists to remove. The dotenv physical-line editor is replaced by a patch of the parsed document, so comments and untouched entries keep their formatting, any string value round-trips (multi-line included), and no entry is unwritable for want of a quoting style. The writer lock, read-modify-write, atomic `0600` write under a `0700` directory, exact-path watcher, content-equality self-write suppression, and quiescent disposal are unchanged. -**`$DSH_HOME/.env` is the user's ordinary environment layer.** `loadLayeredEnv` in [`dsh-app-boot`](../../../../packages/ui/app-boot/README.md) parses the invoking directory's `.env` and then the Harness home's, giving `user < project < inherited` by materializing each accepted value only when the process has no higher-layer value. The Harness home is resolved from the inherited environment *before* either file loads, so a project `.env` cannot redirect which user document is read. Only the product CLI layers these files; SDK and example bins keep loading their own directory through `loadEnv` and must not inherit a developer's `$DSH_HOME`. +**`$DSH_HOME/.env` is the user's ordinary environment layer.** `loadLayeredEnv` in [`dsh-app-boot`](../../../../packages/boot/app-boot/README.md) parses the invoking directory's `.env` and then the Harness home's, giving `user < project < inherited` by materializing each accepted value only when the process has no higher-layer value. The Harness home is resolved from the inherited environment *before* either file loads, so a project `.env` cannot redirect which user document is read. Only the product CLI layers these files; SDK and example bins keep loading their own directory through `loadEnv` and must not inherit a developer's `$DSH_HOME`. Credential precedence distinguishes the inherited environment from discovered files: the inherited value stays the read-only per-run override, the managed document wins next, and project then user `.env` values remain writable fallbacks. A `set` therefore replaces a discovered-file value instead of rejecting a write that only the flattened `process.env` view would consider shadowed. @@ -38,7 +38,7 @@ There is no migration. A key already in `$DSH_HOME/.env` keeps resolving as a fa ## Alternatives considered -**Keep one `$DSH_HOME/.env` and teach the CLI to hoist it.** Rejected: hoisting the store is precisely what makes stored keys unrotatable, which is why [app-boot documented the exclusion](../../../../packages/ui/app-boot/README.md) in the first place. The conflict is the file's two jobs, not the loader. +**Keep one `$DSH_HOME/.env` and teach the CLI to hoist it.** Rejected: hoisting the store is precisely what makes stored keys unrotatable, which is why [app-boot documented the exclusion](../../../../packages/boot/app-boot/README.md) in the first place. The conflict is the file's two jobs, not the loader. **`$DSH_HOME/.credentials.env` — a second dotenv file.** Rejected: dotenv suits an environment layer but cannot express "a managed document indexed by credential reference". It cannot reject a non-string or an unaddressable key, and its line editor already refused values it could not quote, leaving entries readable but unwritable. diff --git a/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.zh.md b/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.zh.md index 370179b442..5f724c7a12 100644 --- a/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.zh.md @@ -23,7 +23,7 @@ OPENAI_API_KEY: sk-… 因为该文档只存放凭据、别无他物,任何偏离都是拒绝而不是跳过条目:非 mapping 的根、非 POSIX 标识符的键、非字符串值、空字符串、重复键以及格式错误的 YAML 全部失败——启动时和写入时响亮失败,运行期热重载则告警并保留最后可用快照。被静默忽略的键读起来就是「我存进去的密钥没有生效」,而这正是本次变更要消除的失败。dotenv 物理行编辑器被替换为对已解析文档打补丁,因此注释与未触及条目的排版都会保留,任何字符串值都能往返(含多行),也不会再有条目因为缺少可用引号样式而不可写。写锁、read-modify-write、`0700` 目录下的 `0600` 原子写、精确路径 watcher、按内容相等抑制自写、以及 dispose 时的完全停稳,均保持不变。 -**`$DSH_HOME/.env` 是用户的普通环境层。** [`dsh-app-boot`](../../../../packages/ui/app-boot/README.md) 中的 `loadLayeredEnv` 先解析调用目录的 `.env`,再解析 Harness home 的,并且只在进程中没有更高层值时物化每个已接受的值,从而得到 `用户 < 项目 < 继承`。Harness home 在两个文件加载*之前*就从继承的环境解析完毕,因此项目 `.env` 无法改变读取哪份用户文档。只有产品 CLI(命令行界面)叠加这两个文件;SDK 与示例 bin 仍通过 `loadEnv` 加载各自的目录,绝不继承开发者的 `$DSH_HOME`。 +**`$DSH_HOME/.env` 是用户的普通环境层。** [`dsh-app-boot`](../../../../packages/boot/app-boot/README.md) 中的 `loadLayeredEnv` 先解析调用目录的 `.env`,再解析 Harness home 的,并且只在进程中没有更高层值时物化每个已接受的值,从而得到 `用户 < 项目 < 继承`。Harness home 在两个文件加载*之前*就从继承的环境解析完毕,因此项目 `.env` 无法改变读取哪份用户文档。只有产品 CLI(命令行界面)叠加这两个文件;SDK 与示例 bin 仍通过 `loadEnv` 加载各自的目录,绝不继承开发者的 `$DSH_HOME`。 凭据优先级会区分继承环境与发现的文件:继承值仍是只读的按次覆盖,其后是受管文档,再后是仍可写的项目与用户 `.env` 后备值。因此 `set` 会替换发现文件中的值,而不是因为扁平化的 `process.env` 视图认为写入会被遮蔽就加以拒绝。 @@ -38,7 +38,7 @@ OPENAI_API_KEY: sk-… ## Alternatives considered -**保留单一的 `$DSH_HOME/.env`,让 CLI 去提升它。** 否决:提升存储本身正是让已存密钥无法轮换的原因,这也是 [app-boot 当初记录该排除](../../../../packages/ui/app-boot/README.md)的理由。冲突来自这个文件的两份工作,而不是加载器。 +**保留单一的 `$DSH_HOME/.env`,让 CLI 去提升它。** 否决:提升存储本身正是让已存密钥无法轮换的原因,这也是 [app-boot 当初记录该排除](../../../../packages/boot/app-boot/README.md)的理由。冲突来自这个文件的两份工作,而不是加载器。 **`$DSH_HOME/.credentials.env`——第二个 dotenv 文件。** 否决:dotenv 适合环境层,却无法表达「一份按凭据引用索引的受管文档」。它无法拒绝非字符串或无法寻址的键,而且它的行编辑器本来就会拒绝无法加引号的值,留下可读却不可写的条目。 diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.i18n.yaml index 6f4a6d363d..6e2c9a635d 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.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/bug-fix/2026-07-20-config-hot-reload-resilience.md -2026-07-20-config-hot-reload-resilience.md: 0f15bb0aaacb6e06c416cbe35b44155279497eee -2026-07-20-config-hot-reload-resilience.zh.md: 8a185c1915b5247150d8bb1dd5c42d69bd4f2a35 +2026-07-20-config-hot-reload-resilience.md: 2b98ee73e1c7734da9c0ae6df5e3de1af6a5cbeb +2026-07-20-config-hot-reload-resilience.zh.md: 4ddf1f07f4ff5449f3bd91aa2b38f6045eb4e1c6 diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.md b/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.md index 0f15bb0aaa..2b98ee73e1 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.md +++ b/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.md @@ -38,4 +38,4 @@ HMR contains live refresh rejection. Its `registerConfig(filename, refresh)` met ## Testing -`packages/ui/app-boot/tests/config-reload.spec.ts` boots real temporary Loader/Include trees and covers parse and shape rejection, import-before-dispose, plugin/config restoration, multi-entry rollback, ancestor disablement, overlay convergence, option identity, failed direct-update persistence, and failed programmatic moves. `packages/ui/app-boot/tests/hmr-config.spec.ts` covers existing and missing exact paths, add/change/removal, serialized coalescing, disposal drainage, non-`Error` normalization, failure broadcast, and rejecting-observer containment. `packages/host/webserver/tests/webserver.spec.ts` proves a service-gated startup failure rejects Loader composition with its bind diagnostic, `packages/typert/loader/tests/loader.spec.ts` exercises awaited programmatic removal through a real Loader consumer, and the ACP `pty-tools` snapshot guards concurrent composition from reordering equal-priority prompt sections. +`packages/boot/app-boot/tests/config-reload.spec.ts` boots real temporary Loader/Include trees and covers parse and shape rejection, import-before-dispose, plugin/config restoration, multi-entry rollback, ancestor disablement, overlay convergence, option identity, failed direct-update persistence, and failed programmatic moves. `packages/boot/app-boot/tests/hmr-config.spec.ts` covers existing and missing exact paths, add/change/removal, serialized coalescing, disposal drainage, non-`Error` normalization, failure broadcast, and rejecting-observer containment. `packages/host/webserver/tests/webserver.spec.ts` proves a service-gated startup failure rejects Loader composition with its bind diagnostic, `packages/typert/loader/tests/loader.spec.ts` exercises awaited programmatic removal through a real Loader consumer, and the ACP `pty-tools` snapshot guards concurrent composition from reordering equal-priority prompt sections. diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.zh.md b/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.zh.md index 8a185c1915..4ddf1f07f4 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.zh.md @@ -38,4 +38,4 @@ HMR 收容实时刷新 rejection。其 `registerConfig(filename, refresh)` 方 ## Testing -`packages/ui/app-boot/tests/config-reload.spec.ts` 启动真实的临时 Loader/Include 树,并覆盖对解析和形状错误的拒绝、先导入再 dispose、插件/配置恢复、多配置项回滚、祖先禁用、overlay 收敛、option 对象身份、失败的直接更新不持久化以及失败的程序化移动。`packages/ui/app-boot/tests/hmr-config.spec.ts` 覆盖现有和缺失的确切路径、添加/变更/移除、串行化合并、dispose 排空、非 `Error` 值的规范化、失败广播以及对发生 rejection 的观察者的收容。`packages/host/webserver/tests/webserver.spec.ts` 证明受服务门控的启动失败会让 Loader 组合以其 bind 诊断 reject;`packages/typert/loader/tests/loader.spec.ts` 则通过真实 Loader 消费方演练可等待的程序化移除;ACP(Agent Client Protocol)的 `pty-tools` 快照会防止并发组合改变同优先级提示词段的顺序。 +`packages/boot/app-boot/tests/config-reload.spec.ts` 启动真实的临时 Loader/Include 树,并覆盖对解析和形状错误的拒绝、先导入再 dispose、插件/配置恢复、多配置项回滚、祖先禁用、overlay 收敛、option 对象身份、失败的直接更新不持久化以及失败的程序化移动。`packages/boot/app-boot/tests/hmr-config.spec.ts` 覆盖现有和缺失的确切路径、添加/变更/移除、串行化合并、dispose 排空、非 `Error` 值的规范化、失败广播以及对发生 rejection 的观察者的收容。`packages/host/webserver/tests/webserver.spec.ts` 证明受服务门控的启动失败会让 Loader 组合以其 bind 诊断 reject;`packages/typert/loader/tests/loader.spec.ts` 则通过真实 Loader 消费方演练可等待的程序化移除;ACP(Agent Client Protocol)的 `pty-tools` 快照会防止并发组合改变同优先级提示词段的顺序。 diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.i18n.yaml index 4931fa907b..fda3830670 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.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/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md -2026-07-31-fail-loud-releases-the-terminal.md: 2a6e7fcbbdd5d35bcf70dee09fdb9e5592486b78 -2026-07-31-fail-loud-releases-the-terminal.zh.md: f75c21cf79b241e6714c10ec7df9ac25f3d978b4 +2026-07-31-fail-loud-releases-the-terminal.md: 5fe812e5fe64a1fab8ba9808d87f3581b6f5b206 +2026-07-31-fail-loud-releases-the-terminal.zh.md: 64459614dc3376a1c773de72cd36aa455894a03c diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md index 2a6e7fcbbd..5fe812e5fe 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md +++ b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md @@ -52,7 +52,7 @@ The guarantee belongs to whichever bin owns the terminal: a surface that grabs t ## Testing -`packages/ui/app-boot/tests/app-boot.spec.ts` covers the release contract: the hook is awaited before the exit commits, a rejecting hook still exits 1, a never-settling hook exits after `FAIL_LOUD_RELEASE_TIMEOUT_MS`, and a burst of rejections reports only the first while the release still completes. +`packages/boot/app-boot/tests/app-boot.spec.ts` covers the release contract: the hook is awaited before the exit commits, a rejecting hook still exits 1, a never-settling hook exits after `FAIL_LOUD_RELEASE_TIMEOUT_MS`, and a burst of rejections reports only the first while the release still completes. Those fake-process tests cannot observe the two failure modes that matter most — process exit code with a real event loop, and terminal state after exit — so the regression lives in `apps/cli/tests/tui-keyless-smoke.e2e.ts`. It boots the shipped tree in a real PTY over `fixtures/tui-invalid-provider.cordis.yml` (a list-shaped `providers`, the mistake users actually make), expects exit 1, and asserts the captured bytes contain both the labelled boot rejection (`dsh: plugin tree failed to load:`) and `ESC[?2004l`. The same case pins the boot path end to end: it caught the [HMR initial-scan boot deadlock](2026-08-03-hmr-initial-scan-boot-deadlock.md) that silently exited 13 with the terminal stranded. diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md index f75c21cf79..64459614dc 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md @@ -52,7 +52,7 @@ Loader 并发挂载各个条目,因此条目失败的顺序并不等于启动 ## Testing -`packages/ui/app-boot/tests/app-boot.spec.ts` 覆盖 release 契约:退出提交前会等待该回调;回调 rejection 时仍退出 1;永不结算的回调会在 `FAIL_LOUD_RELEASE_TIMEOUT_MS` 后退出;以及一连串 rejection 只报告第一个,同时 release 仍能跑完。 +`packages/boot/app-boot/tests/app-boot.spec.ts` 覆盖 release 契约:退出提交前会等待该回调;回调 rejection 时仍退出 1;永不结算的回调会在 `FAIL_LOUD_RELEASE_TIMEOUT_MS` 后退出;以及一连串 rejection 只报告第一个,同时 release 仍能跑完。 这些基于假进程的测试无法观测到最关键的两种失败形态——真实事件循环下的进程退出码,以及退出之后的终端状态——因此回归用例放在 `apps/cli/tests/tui-keyless-smoke.e2e.ts`。它在真实 PTY 中以 `fixtures/tui-invalid-provider.cordis.yml`(`providers` 为列表形状,正是用户真实会犯的错误)启动出厂配置树,期望退出码为 1,并断言捕获到的字节流同时包含带标签的启动 rejection(`dsh: plugin tree failed to load:`)与 `ESC[?2004l`。同一用例端到端钉住了启动路径:正是它发现了以 13 静默退出、终端状态被残留的 [HMR 初始扫描启动死锁](2026-08-03-hmr-initial-scan-boot-deadlock.md)。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.i18n.yaml index 4752010ef6..36c7895ac2 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.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/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md -2026-08-03-cli-signal-shutdown-escalation.md: 7c9715c37ee57be9fa0f67af0c19f0bfa84845da -2026-08-03-cli-signal-shutdown-escalation.zh.md: f3485edc9e453c0b774f442bfce6d678d63f2224 +2026-08-03-cli-signal-shutdown-escalation.md: c8aac6e2be927bd1f4a445c00e0aaa870b10a465 +2026-08-03-cli-signal-shutdown-escalation.zh.md: 66fe7062ea35a6cc91dbdd6644088cb086a15439 diff --git a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md index 7c9715c37e..c8aac6e2be 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md +++ b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md @@ -49,4 +49,4 @@ The controller is launcher infrastructure rather than a Cordis plugin: it makes `apps/cli/tests/headless-shutdown.e2e.ts` boots the real shipped Web/headless Loader tree in a PTY with a test-only plugin whose disposer announces entry and never settles. The test sends SIGINT after the observation URL, waits for proof that disposal started, sends SIGINT again, and requires exit 130. The source/artifact launch resolver keeps the same regression on both execution planes. This PTY case covers the user-visible process state; no model-output snapshot changes. -`packages/telemetry/session-telemetry-otel/tests/otel.spec.ts` holds a real OTLP request open after timer export begins and pins that Cordis disposal returns at `shutdownTimeoutMillis`, despite the SDK's `forceFlush()` remaining pending. The collector is then released so the still-observed provider Promise settles cleanly. +`packages/session/session-telemetry-otel/tests/otel.spec.ts` holds a real OTLP request open after timer export begins and pins that Cordis disposal returns at `shutdownTimeoutMillis`, despite the SDK's `forceFlush()` remaining pending. The collector is then released so the still-observed provider Promise settles cleanly. diff --git a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md index f3485edc9e..66fe7062ea 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md @@ -49,4 +49,4 @@ headless 对完成的轮次仍以 0 退出,对其他轮次结束原因或 API `apps/cli/tests/headless-shutdown.e2e.ts` 在 PTY 中启动真实交付的 Web/headless Loader 插件树,并挂载一个仅用于测试的插件;该插件的 disposer 会声明已经进入清理流程,但永不结算。测试在观察地址出现后发送 SIGINT,等待 dispose 已启动的证据,再次发送 SIGINT,并要求进程以 130 退出。源码/产物启动解析器使两个执行平面都覆盖同一项回归。该 PTY 用例覆盖用户可见的进程状态;模型输出快照没有变化。 -`packages/telemetry/session-telemetry-otel/tests/otel.spec.ts` 在定时器导出开始后保持一条真实 OTLP 请求打开,并固定以下行为:即使 SDK 的 `forceFlush()` 仍待结算,Cordis dispose 也会在 `shutdownTimeoutMillis` 到期时返回。随后测试释放 collector,使仍受观察的提供方 Promise 干净结算。 +`packages/session/session-telemetry-otel/tests/otel.spec.ts` 在定时器导出开始后保持一条真实 OTLP 请求打开,并固定以下行为:即使 SDK 的 `forceFlush()` 仍待结算,Cordis dispose 也会在 `shutdownTimeoutMillis` 到期时返回。随后测试释放 collector,使仍受观察的提供方 Promise 干净结算。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.i18n.yaml index 170627ea76..51fbed7ba9 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.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/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.md -2026-08-03-hmr-initial-scan-boot-deadlock.md: 4b3e259c216d258c321ab06c41225b33ed240d19 -2026-08-03-hmr-initial-scan-boot-deadlock.zh.md: ce1bc8396ac6e7fb6ecb1647fe2b29cdc788c7e1 +2026-08-03-hmr-initial-scan-boot-deadlock.md: 1a70fbafc74a1bf7024c675ddad82acefc995b72 +2026-08-03-hmr-initial-scan-boot-deadlock.zh.md: 5f8d50c4c0103eb2209eafb89cf34233c4c1bc35 diff --git a/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.md b/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.md index 4b3e259c21..1a70fbafc7 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.md +++ b/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.md @@ -38,4 +38,4 @@ One latent gap remains: a config edit made during a *failing* initial apply can ## Testing -The `dsh` invalid-provider PTY case in `apps/cli/tests/tui-keyless-smoke.e2e.ts` pins the end-to-end contract: exit 1, the labelled `dsh: plugin tree failed to load:` diagnostic naming `$.providers`, and the bracketed-paste reset proving the tree was disposed. Before this fix the same case observed exit 13 with no diagnostic. Reload behavior stays covered by `packages/ui/app-boot/tests/config-reload.spec.ts` and `packages/ui/app-boot/tests/hmr-config.spec.ts`. +The `dsh` invalid-provider PTY case in `apps/cli/tests/tui-keyless-smoke.e2e.ts` pins the end-to-end contract: exit 1, the labelled `dsh: plugin tree failed to load:` diagnostic naming `$.providers`, and the bracketed-paste reset proving the tree was disposed. Before this fix the same case observed exit 13 with no diagnostic. Reload behavior stays covered by `packages/boot/app-boot/tests/config-reload.spec.ts` and `packages/boot/app-boot/tests/hmr-config.spec.ts`. diff --git a/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.zh.md b/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.zh.md index ce1bc8396a..5f8d50c4c0 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.zh.md @@ -38,4 +38,4 @@ ## 测试 -`apps/cli/tests/tui-keyless-smoke.e2e.ts` 中 `dsh` 无效 provider 的 PTY 用例钉住了端到端契约:以 1 退出、带标签的 `dsh: plugin tree failed to load:` 诊断指明 `$.providers`、以及证明整棵树已被释放的 bracketed-paste 复位序列。此修复之前,同一用例观察到的是无诊断的 exit 13。重载行为仍由 `packages/ui/app-boot/tests/config-reload.spec.ts` 与 `packages/ui/app-boot/tests/hmr-config.spec.ts` 覆盖。 +`apps/cli/tests/tui-keyless-smoke.e2e.ts` 中 `dsh` 无效 provider 的 PTY 用例钉住了端到端契约:以 1 退出、带标签的 `dsh: plugin tree failed to load:` 诊断指明 `$.providers`、以及证明整棵树已被释放的 bracketed-paste 复位序列。此修复之前,同一用例观察到的是无诊断的 exit 13。重载行为仍由 `packages/boot/app-boot/tests/config-reload.spec.ts` 与 `packages/boot/app-boot/tests/hmr-config.spec.ts` 覆盖。 diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml b/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml index 3be447fe2f..8481cc20d5 100644 --- a/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.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-06-30-interception-seams.md -2026-06-30-interception-seams.md: c318e41cfb1d64230b6151f1febad85d75b1451d -2026-06-30-interception-seams.zh.md: 1b274fae4bc7fde326dbb0eeec54d57f73987803 +2026-06-30-interception-seams.md: 3b2c62cd4413f6d93cef247c51304a8d46753218 +2026-06-30-interception-seams.zh.md: 4e7b6a5ac9ca88919246508cf86c486b06888417 diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-seams.md b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md index c318e41cfb..3b2c62cd44 100644 --- a/.agents/notes/implemented/feature/2026-06-30-interception-seams.md +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md @@ -56,4 +56,4 @@ The seam package does **not** declare `hook/*` session events (the durable hook- ## Consequences -The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, pre-step claim settlement, post-tool context buffering, and stopping; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge settles an initial pre-step rejection from its blocked no-step turn as `end_turn`, while hook-driven snapshots verify the observable bridge behavior end to end. +The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, pre-step claim settlement, post-tool context buffering, and stopping; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/subsystems/core.md#interception-decisions), and [tool structures](../../../../docs/subsystems/tools.md). The ACP bridge settles an initial pre-step rejection from its blocked no-step turn as `end_turn`, while hook-driven snapshots verify the observable bridge behavior end to end. diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md b/.agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md index 1b274fae4b..4e7b6a5ac9 100644 --- a/.agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md @@ -56,4 +56,4 @@ seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志); ## 后果 -规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层做包装,终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、pre-step 领取结算、工具执行后上下文缓冲和 stopping;`dsh-tools` 负责身份封存与五阶段执行流水线。它们的契约记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/core-data-structures/core.md#interception-decisions) 与[工具结构](../../../../docs/core-data-structures/tools.md)中。ACP 桥接会把 blocked 无步骤轮次中的首次 pre-step reject 结算为 `end_turn`,而钩子驱动的快照端到端验证可观测的桥接行为。 +规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层做包装,终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、pre-step 领取结算、工具执行后上下文缓冲和 stopping;`dsh-tools` 负责身份封存与五阶段执行流水线。它们的契约记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/subsystems/core.md#interception-decisions) 与[工具结构](../../../../docs/subsystems/tools.md)中。ACP 桥接会把 blocked 无步骤轮次中的首次 pre-step reject 结算为 `end_turn`,而钩子驱动的快照端到端验证可观测的桥接行为。 diff --git a/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.i18n.yaml b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.i18n.yaml index 15054d6ba1..394af669cd 100644 --- a/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.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-dynamic-workflows.md -2026-07-05-dynamic-workflows.md: bba62098c66477a3f1929f9029e81c645bfc4d41 -2026-07-05-dynamic-workflows.zh.md: 2005ca14883ae137148541273900c7f3e65769a5 +2026-07-05-dynamic-workflows.md: b0fb2349e64093beab8220ea75715b55903772e3 +2026-07-05-dynamic-workflows.zh.md: 246727649be8acc17dac1335d353e9b1591e3890 diff --git a/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md index bba62098c6..b0fb2349e6 100644 --- a/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md +++ b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md @@ -20,7 +20,7 @@ One deliberate strictness DIVERGENCE from CC: hook misuse — unknown or deferre ### The seam (dsh-workflow) -`ctx.workflows` is an abstract `WorkflowService` in the bash shape — one engine per context, no named-provider registry (engines are deployment swaps, not co-residents). `start(request)` throws synchronously for a script that cannot begin; a returned `WorkflowRun`'s `result` NEVER rejects (failures resolve as `stopReason: 'error' | 'cancelled'`). The `workflow/*` events are observe-only emits carrying DATA SNAPSHOTS (id + meta; `workflow/end` omits the result value), per-listener contained, mirroring `subagent/start`/`subagent/end` — control stays with the run's holder. Vocabulary details: [core-data-structures/workflow.md](../../../../docs/core-data-structures/workflow.md). +`ctx.workflows` is an abstract `WorkflowService` in the bash shape — one engine per context, no named-provider registry (engines are deployment swaps, not co-residents). `start(request)` throws synchronously for a script that cannot begin; a returned `WorkflowRun`'s `result` NEVER rejects (failures resolve as `stopReason: 'error' | 'cancelled'`). The `workflow/*` events are observe-only emits carrying DATA SNAPSHOTS (id + meta; `workflow/end` omits the result value), per-listener contained, mirroring `subagent/start`/`subagent/end` — control stays with the run's holder. Vocabulary details: [subsystems/workflow.md](../../../../docs/subsystems/workflow.md). ### The engine (dsh-workflow-workerthread): one worker thread per run diff --git a/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.zh.md b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.zh.md index 2005ca1488..246727649b 100644 --- a/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.zh.md +++ b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.zh.md @@ -20,7 +20,7 @@ harness 可以将一个任务委派给一个子 agent(`dsh-tool-subagent`) ### seam(dsh-workflow) -`ctx.workflows` 是 bash 形态的抽象 `WorkflowService`——每个上下文一个引擎,无命名提供方注册表(引擎是部署级替换,不是共存者)。`start(request)` 对无法启动的脚本同步抛出;返回的 `WorkflowRun` 的 `result` 永不 reject(失败时结算为 `stopReason: 'error' | 'cancelled'`)。`workflow/*` 事件是仅观察的 emit,携带数据快照(id + meta;`workflow/end` 省略 result 值),按监听器隔离,与 `subagent/start`/`subagent/end` 对称——控制权留在 run 的持有者手中。词汇详情见 [core-data-structures/workflow.md](../../../../docs/core-data-structures/workflow.md)。 +`ctx.workflows` 是 bash 形态的抽象 `WorkflowService`——每个上下文一个引擎,无命名提供方注册表(引擎是部署级替换,不是共存者)。`start(request)` 对无法启动的脚本同步抛出;返回的 `WorkflowRun` 的 `result` 永不 reject(失败时结算为 `stopReason: 'error' | 'cancelled'`)。`workflow/*` 事件是仅观察的 emit,携带数据快照(id + meta;`workflow/end` 省略 result 值),按监听器隔离,与 `subagent/start`/`subagent/end` 对称——控制权留在 run 的持有者手中。词汇详情见 [subsystems/workflow.md](../../../../docs/subsystems/workflow.md)。 ### 引擎(dsh-workflow-workerthread):每次运行一个 worker 线程 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 a98beff699..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: a998d70ec934aed4bf7ce32aa711abd47b508a1d -2026-07-05-skill-system.zh.md: 4fa7c4fd657c2f41f16b30679ec95e61a75f8a0c +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 a998d70ec9..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/core-data-structures/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 4fa7c4fd65..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/core-data-structures/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/feature/2026-07-06-approval-seam.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml index fa097ae230..b5c6343382 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.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-06-approval-seam.md -2026-07-06-approval-seam.md: ae143a41302b7bcd6029345fa91ca4eda837c141 -2026-07-06-approval-seam.zh.md: 2f95f67ecd33b4c659e56d5f3cc6168bf01e35ce +2026-07-06-approval-seam.md: a286e07d51a70232f9aab0af5d8a19622cd222d4 +2026-07-06-approval-seam.zh.md: 6656cfe46de663f0e89eacc78dcfb3425812f9e0 diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md index ae143a4130..a286e07d51 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md @@ -12,7 +12,7 @@ The routing problem is ownership: a permission request must reach the channel th ## Decision -One package, `dsh-user-approval` (`packages/ui/user-approval`), owns the vocabulary and the `ctx.approval` service — the mechanism. The policy — who answers, and whether a session is asked at all — lives outside it: answerers are `approval/request` waterfall listeners registered by channel-owning plugins (the ACP bridge, host adapters, and test scripts), and a per-session policy tier can decide before a channel is involved. Consumers (`dsh-tools`' ask routing and the sandbox escalation gate) resolve a question to a closed outcome and derive their own tool results from it. This is deliberately one package, not the capability-seam three (see Alternatives). +One package, `dsh-user-approval` (`packages/interaction/user-approval`), owns the vocabulary and the `ctx.approval` service — the mechanism. The policy — who answers, and whether a session is asked at all — lives outside it: answerers are `approval/request` waterfall listeners registered by channel-owning plugins (the ACP bridge, host adapters, and test scripts), and a per-session policy tier can decide before a channel is involved. Consumers (`dsh-tools`' ask routing and the sandbox escalation gate) resolve a question to a closed outcome and derive their own tool results from it. This is deliberately one package, not the capability-seam three (see Alternatives). ### How a deployment uses it @@ -95,7 +95,7 @@ Snapshots record allowed and rejected sandbox escalation through `session/reques - **A single registered provider instead of waterfall listeners** — rejected: a `registerProvider()` surface forces every composition question — allowlist pre-filters, external hook deciders, scripted test answers, a policy gate in front of a human — inside one provider implementation. The waterfall gets composition, fail-closed absence, and HMR disposal from machinery the runtime already has; the seam's JSDoc pins the single-decision-slot convention instead of inventing a provider registry. - **An inline `tools/pre-execute` permission gate in the ACP bridge** — rejected: prompting for every bridge-owned call hardwires the asking policy into the transport, cannot serve a second asker (sandbox escalation happens after execution starts, with no pre-execute moment), and leaves hook-produced `ask` decisions without a shared mechanism. -- **The generic user-interaction seam (`ctx.userInteraction`)** — rejected as the approval mechanism: the two share a skeleton (route by agent, block for a human, handle absence), but approval's contract is narrower in every dimension that matters: a closed outcome vocabulary instead of free text, a protocol-native prompt attached to a tool call instead of a generic form, mandatory fail-closed absence, and audit events. Approval therefore does not ride the shipped `packages/ui/user-interaction` / `ask_user_question` elicitation path — an elicitation form is not a permission prompt, and a free-text answer is not a closed outcome; sharing provider plumbing stays open if the two ever converge. +- **The generic user-interaction seam (`ctx.userInteraction`)** — rejected as the approval mechanism: the two share a skeleton (route by agent, block for a human, handle absence), but approval's contract is narrower in every dimension that matters: a closed outcome vocabulary instead of free text, a protocol-native prompt attached to a tool call instead of a generic form, mandatory fail-closed absence, and audit events. Approval therefore does not ride the shipped `packages/interaction/user-interaction` / `ask_user_question` elicitation path — an elicitation form is not a permission prompt, and a free-text answer is not a closed outcome; sharing provider plumbing stays open if the two ever converge. - **Static optional injection in `dsh-tools`** — rejected: the vendored cordis `Inject` type has no optional flag — the object form maps service names to intercept config, and a declared inject gates the fiber. `ctx.get('approval')` is the documented opportunistic-consumption pattern (the `tool-bash` owner-token lookup, the loop's persistence probe), reads presence per call, and degrades correctly across HMR without extra machinery. - **The capability-seam three-package split** — rejected: interface/implementation/consumer fits a seam whose implementation is swappable (bash-local vs bash-sandbox). Here the service body is fixed mechanism and the variable part is listeners that live with their owners — splitting would manufacture an implementation package with nothing in it ("don't split preemptively"). - **Offering `allow_always` now** — rejected: the protocol can express it, but honoring it means designing grant storage, scope identity, and revocation (§ Deferred). Advertising an option the harness cannot honor manufactures doomed grants. diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md index 2f95f67ecd..6656cfe46d 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -一个包`dsh-user-approval`(`packages/ui/user-approval`)负责定义词汇表和 `ctx.approval` 服务——即机制。策略——谁来应答、某个会话是否需要被询问——不在其中:应答者是 `approval/request` waterfall 监听器,由拥有通道的插件注册(ACP(Agent Client Protocol)桥、宿主适配器、测试脚本),而每会话的策略层可以在任何通道介入之前做出决定。消费方(`dsh-tools` 的 ask 路由和沙箱升级门禁)将问题解析为一个封闭结果,并从中派生各自的工具结果。刻意设计为一个包,而非能力 seam 的三包拆分(见「替代方案」)。 +一个包`dsh-user-approval`(`packages/interaction/user-approval`)负责定义词汇表和 `ctx.approval` 服务——即机制。策略——谁来应答、某个会话是否需要被询问——不在其中:应答者是 `approval/request` waterfall 监听器,由拥有通道的插件注册(ACP(Agent Client Protocol)桥、宿主适配器、测试脚本),而每会话的策略层可以在任何通道介入之前做出决定。消费方(`dsh-tools` 的 ask 路由和沙箱升级门禁)将问题解析为一个封闭结果,并从中派生各自的工具结果。刻意设计为一个包,而非能力 seam 的三包拆分(见「替代方案」)。 ### 部署如何使用它 @@ -95,7 +95,7 @@ ACP 桥只应答其会话映射所拥有的精确 agent 对象。它携带既有 - **单一注册提供方而非 waterfall 监听器**:否决。`registerProvider()` 接口迫使所有组合问题——允许列表预过滤、外部钩子决策者、脚本化测试应答、人类前面的策略门禁——都塞进一个提供方实现。waterfall 直接复用运行时已有的组合能力、缺失时默认拒绝行为和 HMR(热模块替换)资源释放机制;seam 的 JSDoc 以约定固定单决策槽语义,而非发明一个提供方注册表。 - **在 ACP 桥中内联 `tools/pre-execute` 权限门禁**:否决。对桥拥有的每次调用都弹出提示,会将请求策略硬编码进传输层,无法服务第二个发起方(沙箱升级发生在执行开始之后,没有 pre-execute 时刻),且钩子产生的 `ask` 决策没有共享机制。 -- **通用用户交互 seam(`ctx.userInteraction`)**:否决作为审批机制。二者骨架相似(按 agent 路由、阻塞等待人类、处理缺失),但审批的契约在每个关键维度上都更窄:封闭的结果词汇而非自由文本、附着在工具调用上的协议原生提示而非通用表单、强制的缺失时失败关闭、以及审计事件。因此审批不走已交付的 `packages/ui/user-interaction` / `ask_user_question` 信息征集路径——信息征集表单不是权限提示,自由文本应答不是封闭结果;如果二者将来趋同,共享提供方管道仍然开放。 +- **通用用户交互 seam(`ctx.userInteraction`)**:否决作为审批机制。二者骨架相似(按 agent 路由、阻塞等待人类、处理缺失),但审批的契约在每个关键维度上都更窄:封闭的结果词汇而非自由文本、附着在工具调用上的协议原生提示而非通用表单、强制的缺失时失败关闭、以及审计事件。因此审批不走已交付的 `packages/interaction/user-interaction` / `ask_user_question` 信息征集路径——信息征集表单不是权限提示,自由文本应答不是封闭结果;如果二者将来趋同,共享提供方管道仍然开放。 - **`dsh-tools` 中的静态可选注入**:否决。vendor 的 Cordis `Inject` 类型没有 optional 标志——对象形式将服务名映射到拦截配置,声明的 inject 会阻塞 fiber。`ctx.get('approval')` 是文档化的机会性消费模式(`tool-bash` 的 owner-token 查找、loop 的持久化探测),按调用读取存在性,跨 HMR 正确降级,无需额外机制。 - **能力 seam 的三包拆分**:否决。接口/实现/消费方适合实现可替换的 seam(bash-local vs bash-sandbox)。此处服务体是固定机制,可变部分是留在各自通道拥有者插件中的监听器——拆分只会制造一个空的实现包(「不要预防性拆分」)。 - **现在就提供 `allow_always`**:否决。协议能表达它,但兑现它意味着设计授权存储、作用域标识和撤销(§ 延后)。展示 harness 无法兑现的选项只会制造注定失败的授权。 diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml index 7294f47357..00f475040c 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.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-06-sandbox.md -2026-07-06-sandbox.md: 583b388815cd9b2b9cf94ce393839169ce3ffac3 -2026-07-06-sandbox.zh.md: e435b671a42ca5c3ea4f6800bf91d6e006da35d3 +2026-07-06-sandbox.md: 06c5590454a6947030d828f087b92f44207dad6e +2026-07-06-sandbox.zh.md: 8f575e3cd14973aaaaf9254337b985110bc99d6b diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.md index 583b388815..06c5590454 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.md +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.md @@ -200,7 +200,7 @@ Costs and accepted limits: In-repo precedents this design copies or contrasts with: - [The capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md) — the interface/implementation/consumer split and the "don't split preemptively" timing rule the second consumer satisfied. -- The `dsh-bash` request/spec split ([the bash vocabulary catalog](../../../../docs/core-data-structures/bash.md)) — the complete `sandboxPolicy` rides its per-call carrier, and the explicit-`resolve()` defaulting convention. +- The `dsh-bash` request/spec split ([the bash vocabulary catalog](../../../../docs/subsystems/bash.md)) — the complete `sandboxPolicy` rides its per-call carrier, and the explicit-`resolve()` defaulting convention. - [The approval seam Agent Note](2026-07-06-approval-seam.md) — the channel escalation asks through; its answerer waterfall, audit pair, and one-package rationale are recorded there. - [Event-sourced sessions](../architecture/2026-06-11-event-sourced-sessions.md) and [standalone log-only events](../simplification/2026-07-28-remove-synthetic-log-only-turns.md) — the log-as-store foundation the per-session modes fold over, and the explicit durability boundary the anchoring design obeys. - [The interception-seams Agent Note](2026-06-30-interception-seams.md) — the `tools/pre-execute` vocabulary the escalation gate deliberately does not reuse (an escalating call has no pre-execute moment of its own). diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md index e435b671a4..8f575e3cd1 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md @@ -200,7 +200,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层 本设计复制或对比的仓库内先例: - [能力 seam Agent Note](../architecture/2026-06-13-capability-seams.md)——接口/实现/消费方拆分与「不要过早拆分」的时机规则(第二个消费方满足了该规则)。 -- `dsh-bash` 的 request/spec 拆分([bash 词汇目录](../../../../docs/core-data-structures/bash.md))——完整的 `sandboxPolicy` 搭载其按调用载体,以及显式 `resolve()` 默认约定。 +- `dsh-bash` 的 request/spec 拆分([bash 词汇目录](../../../../docs/subsystems/bash.md))——完整的 `sandboxPolicy` 搭载其按调用载体,以及显式 `resolve()` 默认约定。 - [批准 seam Agent Note](2026-07-06-approval-seam.md)——升级请求通过的通道;其应答器 waterfall(瀑布式事件)、审计对和单包理由记录在那里。 - [事件溯源会话](../architecture/2026-06-11-event-sourced-sessions.md)与[独立纯日志事件](../simplification/2026-07-28-remove-synthetic-log-only-turns.md)——按会话模式 fold 所依赖的日志即存储基础,以及锚定设计遵守的显式持久性边界。 - [拦截 seam Agent Note](2026-06-30-interception-seams.md)——`tools/pre-execute` 词汇,升级门控刻意不复用它(升级调用没有自己的 pre-execute 时刻)。 diff --git a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml index 487726c0e3..105a9d3cfd 100644 --- a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.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-08-self-referential-cordis-toolset.md -2026-07-08-self-referential-cordis-toolset.md: 335d5e808016ebc37c8457c5dcf4d7da9d8b8c93 -2026-07-08-self-referential-cordis-toolset.zh.md: 46d492cd92495a1e4d516db3f82c9e75486bc386 +2026-07-08-self-referential-cordis-toolset.md: 29d331008d4111ae00147f2ddd19a628135339ac +2026-07-08-self-referential-cordis-toolset.zh.md: 55013b774b4b6b371d9cbe6ac492b4e95500a7ff diff --git a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md index 335d5e8080..29d331008d 100644 --- a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md +++ b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md @@ -12,7 +12,7 @@ First, model-written registration must be validated where it happens: a malforme ## Decision -The toolset ships as [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/cordis/tool-cordis/README.md) and is demoed by `examples/web-cordis`. It gives the model three tools over the live Cordis runtime in the current DSH process: inspect it, mount an in-memory temporary Plugin, and unmount that Plugin to quiescence. +The toolset ships as [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/self-modification/tool-cordis/README.md) and is demoed by `examples/web-cordis`. It gives the model three tools over the live Cordis runtime in the current DSH process: inspect it, mount an in-memory temporary Plugin, and unmount that Plugin to quiescence. The vm isolates accidental global pollution, and the context façade hides framework internals. Neither restricts the authority of exposed services: a temporary Plugin can call `ctx.bash` with the host executor's privileges and reach the real filesystem and web services. It runs in the shared DSH runtime and may affect other sessions in that process. This is an opt-in development tool with bash-equivalent trust, not a security boundary or product default. diff --git a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md index 46d492cd92..55013b774b 100644 --- a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md +++ b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -该工具集以 [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/cordis/tool-cordis/README.md) 发布,并由 `examples/web-cordis` 演示。它为模型提供三个工具,用于操作当前 DSH 进程中的活跃 Cordis 运行时:检查该运行时、挂载一个仅存于内存的临时 Plugin,再将该 Plugin 卸载至完全停稳。 +该工具集以 [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/self-modification/tool-cordis/README.md) 发布,并由 `examples/web-cordis` 演示。它为模型提供三个工具,用于操作当前 DSH 进程中的活跃 Cordis 运行时:检查该运行时、挂载一个仅存于内存的临时 Plugin,再将该 Plugin 卸载至完全停稳。 vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节。但二者都不限制已暴露服务的权限:临时 Plugin 可以调用 `ctx.bash` 以宿主执行器的权限运行命令,也能访问真实的文件系统和网络服务。它运行在共享 DSH runtime 中,可能影响同一进程的其他 session。这是一个需要显式启用的开发工具,信任等级与 bash 相当,不是安全边界,也不是产品默认配置。 diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml index 3119d28d71..6462a2c873 100644 --- a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.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-10-parallel-tool-call-execution.md -2026-07-10-parallel-tool-call-execution.md: 19f5dc189821433052edfa72613980a2e94e2cae -2026-07-10-parallel-tool-call-execution.zh.md: 69bff90c11fa132ded325ef610dabf5c609f21af +2026-07-10-parallel-tool-call-execution.md: 7fd540539fd358b3254470f420a0cc11f2927d45 +2026-07-10-parallel-tool-call-execution.zh.md: 985557974c79d4080c4e35ccbb9740439988261a diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md index 19f5dc1898..7fd540539f 100644 --- a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md +++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md @@ -14,7 +14,7 @@ The session log remains authoritative: every started call has an audit event, or ## Decision -Each tool may provide an optional `isConcurrencySafe(args)` classifier. It is synchronous and pure: it examines only the current call's parsed arguments and performs no I/O or mutation. Only an explicit `true` opts in; a missing classifier, invalid arguments, a thrown classifier, or any other return value makes the call exclusive. The canonical type contract lives in the [tool data structures](../../../../docs/core-data-structures/tools.md). +Each tool may provide an optional `isConcurrencySafe(args)` classifier. It is synchronous and pure: it examines only the current call's parsed arguments and performs no I/O or mutation. Only an explicit `true` opts in; a missing classifier, invalid arguments, a thrown classifier, or any other return value makes the call exclusive. The canonical type contract lives in the [tool data structures](../../../../docs/subsystems/tools.md). The classifier is deliberately unary. Returning `true` is the tool's promise that this call may overlap with any sibling call that also returns `true`; the scheduler does not compare calls or prove that their resource accesses are compatible. diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md index 69bff90c11..985557974c 100644 --- a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md +++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md @@ -14,7 +14,7 @@ Status: implemented ## 决策 -每个工具都可以提供可选的 `isConcurrencySafe(args)` 分类器。该分类器必须是同步纯函数:它只检查当前调用已解析的参数,不执行 I/O 或任何变更。只有显式返回 `true` 才表示选择并行;分类器缺失、参数无效、分类器抛错或返回任何其他值,都会使该调用按独占方式执行。规范类型契约见[工具数据结构](../../../../docs/core-data-structures/tools.md)。 +每个工具都可以提供可选的 `isConcurrencySafe(args)` 分类器。该分类器必须是同步纯函数:它只检查当前调用已解析的参数,不执行 I/O 或任何变更。只有显式返回 `true` 才表示选择并行;分类器缺失、参数无效、分类器抛错或返回任何其他值,都会使该调用按独占方式执行。规范类型契约见[工具数据结构](../../../../docs/subsystems/tools.md)。 分类器有意设计为一元函数。返回 `true` 表示工具承诺:此调用可以与任何同样返回 `true` 的并列调用重叠执行。调度器不会比较调用,也不会证明它们的资源访问相容。 diff --git a/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.i18n.yaml b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.i18n.yaml index 205ce40bb2..b2e1785796 100644 --- a/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.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-12-subagent-persona-tool-filter-and-depth.md -2026-07-12-subagent-persona-tool-filter-and-depth.md: c690f4701a54272205eedf719fbfe0863bac566c -2026-07-12-subagent-persona-tool-filter-and-depth.zh.md: e14214b16c413eaa05bcea7bcdf9bca996e8d616 +2026-07-12-subagent-persona-tool-filter-and-depth.md: 2c5eaf8829e01536a2de34cc190573ff35e4ab77 +2026-07-12-subagent-persona-tool-filter-and-depth.zh.md: 355db26ecaf57137420798474561d6a7032757cb diff --git a/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md index c690f4701a..2c5eaf8829 100644 --- a/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md +++ b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md @@ -53,7 +53,7 @@ The depth limit bounds recursive delegation independently of tool visibility. A The effective parent depth is the greater of durable `SessionHeader.delegationDepth` and runtime `AgentOptions.subagentDepth`. An in-process child records its derived depth in the session header, and resume restores that header, so a restart cannot lower the recursion count. -Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. A direct `SubagentStartRequest` may omit the cap to leave depth unbounded; loader-resolved `dsh-tool-subagent` configuration instead defaults to `3`, accepts a numeric override, and uses explicit `'provider-managed'` to omit the cap for an out-of-process provider whose deployment owns its recursion budget. Three is a small finite default that still permits a root plus three descendant generations: the [SDK helper's generated subagent entries](../../../../packages/sdk/helper/src/features/builtin/index.ts) and [JSON-RPC example](../../../../examples/jsonrpc-agent/cordis.yml) use that general policy, while the shipped interactive ACP, headless, and REPL examples pin one. A numeric tool cap fails at provider mount when the provider lacks `depthLimit`. +Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. A direct `SubagentStartRequest` may omit the cap to leave depth unbounded; loader-resolved `dsh-tool-subagent` configuration instead defaults to `3`, accepts a numeric override, and uses explicit `'provider-managed'` to omit the cap for an out-of-process provider whose deployment owns its recursion budget. Three is a small finite default that still permits a root plus three descendant generations: the [SDK helper's generated subagent entries](../../../../packages/scaffold/helper/src/features/builtin/index.ts) and [JSON-RPC example](../../../../examples/jsonrpc-agent/cordis.yml) use that general policy, while the shipped interactive ACP, headless, and REPL examples pin one. A numeric tool cap fails at provider mount when the provider lacks `depthLimit`. A deployment can combine depth and filtering, but the numeric cap does not synthesize a filter. The delegation tool stays visible at the cap because authorization may depend on runtime state; every attempted start checks the calling agent's current durable and runtime depth, and a rejected start returns an errored tool result without publishing a child. A deployment may separately deny delegation tools in children when its visibility policy is static. Neither choice changes the provider's conversation-history behavior. diff --git a/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md index e14214b16c..355db26eca 100644 --- a/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md +++ b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md @@ -53,7 +53,7 @@ subagent 启动有三个独立的组合控制:`persona`、`toolFilter` 和 `ma 有效父级深度取持久 `SessionHeader.delegationDepth` 与运行时 `AgentOptions.subagentDepth` 中的较大值。进程内子 agent 把推导出的深度记录在会话 header 中,恢复时会重新载入该 header,因此重启无法降低递归计数。 -每个公开入口都自行验证值域,而非依赖单一的面向模型配置路径。负值、小数、负零、非有限值、不安全整数、格式错误的存储父级深度以及推导溢出均被拒绝。直接的 `SubagentStartRequest` 可以省略上限,让此机制不约束深度;经 loader 解析的 `dsh-tool-subagent` 配置则默认值为 `3`、接受数值覆盖,并使用显式的 `'provider-managed'` 来省略由进程外提供方部署拥有递归预算时的上限。三是一个较小的有限默认值,仍允许 root 加三代后代:[SDK 辅助函数生成的 subagent 条目](../../../../packages/sdk/helper/src/features/builtin/index.ts)和 [JSON-RPC 示例](../../../../examples/jsonrpc-agent/cordis.yml)采用这项通用策略,而已交付的交互式 ACP、headless 和 REPL 示例固定为一。提供方缺少 `depthLimit` 时,数值工具上限会在提供方挂载阶段失败。 +每个公开入口都自行验证值域,而非依赖单一的面向模型配置路径。负值、小数、负零、非有限值、不安全整数、格式错误的存储父级深度以及推导溢出均被拒绝。直接的 `SubagentStartRequest` 可以省略上限,让此机制不约束深度;经 loader 解析的 `dsh-tool-subagent` 配置则默认值为 `3`、接受数值覆盖,并使用显式的 `'provider-managed'` 来省略由进程外提供方部署拥有递归预算时的上限。三是一个较小的有限默认值,仍允许 root 加三代后代:[SDK 辅助函数生成的 subagent 条目](../../../../packages/scaffold/helper/src/features/builtin/index.ts)和 [JSON-RPC 示例](../../../../examples/jsonrpc-agent/cordis.yml)采用这项通用策略,而已交付的交互式 ACP、headless 和 REPL 示例固定为一。提供方缺少 `depthLimit` 时,数值工具上限会在提供方挂载阶段失败。 部署可以组合深度与过滤,但数值上限不会合成过滤器。委派工具在上限处仍然可见,因为授权可能依赖运行时状态;每次尝试启动都会检查调用方 agent 当前的持久与运行时深度,被拒绝的启动返回错误工具结果,且不发布子 agent。可见性策略固定的部署可以另外在子 agent 中 deny 委派工具。两种选择都不改变提供方的对话历史行为。 diff --git a/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.i18n.yaml b/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.i18n.yaml index 673e4869af..a54f2e9496 100644 --- a/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.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-16-harness-level-loop.md -2026-07-16-harness-level-loop.md: 36a567204ee1082d48126369ee0b7277c9f24ca8 -2026-07-16-harness-level-loop.zh.md: cb8d29b4c77848613a6718ea3a343b0ee3294ea4 +2026-07-16-harness-level-loop.md: f37bea0842b3f40bf07c6660ad758d84ece23f1b +2026-07-16-harness-level-loop.zh.md: 0ec2dc2294c1adf8b7f6f810bfaffa7e08c19ed2 diff --git a/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.md b/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.md index 36a567204e..f37bea0842 100644 --- a/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.md +++ b/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.md @@ -38,7 +38,7 @@ Time-based `/loop` or scheduled execution is a third policy and is not implement | `@deepseek-ai/dsh-goal` | `packages/goal/goal/`, domain service | Owns `GoalId`, compare-and-set `GoalRef`, `GoalSnapshot`, four-state `GoalPhase`, structured `GoalBlockReason`, process-local `GoalActivation`, replay folding, and `get`, `create`, `edit`, `pause`, `resume`, `complete`, `block`, `clear`, and `disarm` verbs. | | `@deepseek-ai/dsh-tool-goal` | `packages/goal/tool-goal/`, model-facing consumer | Registers exclusive `get_goal`, `create_goal`, and `update_goal`; authenticates live turn provenance and narrows autonomous-round authority to completion or blocking reports with machine-routable reason codes. | | `@deepseek-ai/dsh-goal-session` | `packages/goal/goal-session/`, continuation policy | Reserves, fences, admits, attributes, settles, cancels, and quiescently drains same-session goal rounds without importing the concrete loop. | -| `@deepseek-ai/dsh-commands` | `packages/ui/commands/`, UI registry | Owns `CommandDefinition`, discovery, scoped registration, direct dispatch, `CommandResult`, and request cancellation for human-only commands. | +| `@deepseek-ai/dsh-commands` | `packages/interaction/commands/`, UI registry | Owns `CommandDefinition`, discovery, scoped registration, direct dispatch, `CommandResult`, and request cancellation for human-only commands. | | `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`, human-command producer | Registers `/goal` status, creation, edit, pause, resume, and clear over the goal domain for TUI. | | `@deepseek-ai/dsh-tool-ralph` | `packages/workflow/tool-ralph/`, fixed workflow consumer | Registers `ralph({ objective, maxRounds? })`, validates the fresh structured provider and bounded `RalphRoundReport`, and returns `complete`, `blocked`, or `budget-limited`. | diff --git a/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.zh.md b/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.zh.md index cb8d29b4c7..0ec2dc2294 100644 --- a/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.zh.md +++ b/.agents/notes/implemented/feature/2026-07-16-harness-level-loop.zh.md @@ -38,7 +38,7 @@ Status: implemented | `@deepseek-ai/dsh-goal` | `packages/goal/goal/`,领域服务 | 拥有 `GoalId`、比较并交换 `GoalRef`、`GoalSnapshot`、四状态 `GoalPhase`、结构化 `GoalBlockReason`、进程本地 `GoalActivation`、重放折叠,以及 `get`、`create`、`edit`、`pause`、`resume`、`complete`、`block`、`clear` 与 `disarm` 动词。 | | `@deepseek-ai/dsh-tool-goal` | `packages/goal/tool-goal/`,面向模型消费方 | 注册互斥的 `get_goal`、`create_goal` 与 `update_goal`;认证实时 Turn 来源,并把自治 Round 权限收窄到带机器可路由原因代码的完成或阻塞报告。 | | `@deepseek-ai/dsh-goal-session` | `packages/goal/goal-session/`,续行策略 | 在不导入具体 loop 的情况下,预留、设围栏、接纳、归属、结算、取消并排空同会话 Goal Round,直至完全停稳。 | -| `@deepseek-ai/dsh-commands` | `packages/ui/commands/`,UI 注册表 | 拥有面向人类专用命令的 `CommandDefinition`、发现、作用域注册、直接分发、`CommandResult` 与请求取消。 | +| `@deepseek-ai/dsh-commands` | `packages/interaction/commands/`,UI 注册表 | 拥有面向人类专用命令的 `CommandDefinition`、发现、作用域注册、直接分发、`CommandResult` 与请求取消。 | | `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`,人类命令生产方 | 为 TUI 注册构建在目标领域之上的 `/goal` 状态、创建、编辑、暂停、恢复与清除。 | | `@deepseek-ai/dsh-tool-ralph` | `packages/workflow/tool-ralph/`,固定工作流消费方 | 注册 `ralph({ objective, maxRounds? })`,验证全新结构化 provider 与有界 `RalphRoundReport`,并返回 `complete`、`blocked` 或 `budget-limited`。 | diff --git a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.i18n.yaml b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.i18n.yaml index 72944d7433..7756f7803c 100644 --- a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.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-16-persistent-pty-sessions.md -2026-07-16-persistent-pty-sessions.md: 88296a9318b2c398fead382153bd2c652dbd9e68 -2026-07-16-persistent-pty-sessions.zh.md: 40f26c67fe046ad292f05919501600bd2e146c62 +2026-07-16-persistent-pty-sessions.md: 219f48235d527cffbf66020ceb22e80ca473093d +2026-07-16-persistent-pty-sessions.zh.md: 18b81cd6a8d3f9cdbaa8566e1b166c3877ae8742 diff --git a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md index 88296a9318..219f48235d 100644 --- a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md +++ b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md @@ -160,7 +160,7 @@ The package ships concise tool guidance explaining persistent state, owner isola - Subprocess process fixtures cover non-leader and non-main-thread stdin waits, zombie quiescence, unreadable process state, supported syscall tables, unsupported architectures, and false-positive rejection; macOS inspector logic is injected into the same unit suite. - Real `node-pty` and PTY-consumer tests jointly exercise shell state, shared sandbox policy, environment scrubbing, raw-mode foreground `SIGINT`, a TERM-ignoring descendant, and immediate post-disposal quiescence on supported hosts. - A Loader-driven `cordis.yml` test mounts the real three-package composition. ACP and headless snapshots pin the six schemas, bounded results, and errors through opt-in overlays; TUI snapshots pin terminal and generic card presentation. -- Package contracts, the architecture map, core data structures, generated catalogs, and the website API describe the same shipped surface. +- Package contracts, the architecture map, subsystem pages, generated catalogs, and the website API describe the same shipped surface. - The repository CI-equivalent sequence owns type, lint, coverage, snapshot, documentation, build, hygiene, demo, and built-entry verification. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md index 40f26c67fe..18b81cd6a8 100644 --- a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md +++ b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md @@ -160,7 +160,7 @@ plugins: - 子进程 fixture 覆盖非 leader 与非主线程的 stdin 等待、僵尸进程完全停稳、不可读进程状态、受支持的 syscall 表、不支持的架构和误报拒绝;同一单元测试套件通过注入覆盖 macOS 检查器逻辑。 - 真实 `node-pty` 与 PTY 消费方测试共同在受支持宿主上覆盖 shell 状态、共享沙箱策略、环境清洗、raw mode 前台 `SIGINT`、忽略 `SIGTERM` 的后代进程,以及 dispose 返回后立即完全停稳。 - Loader 驱动的 `cordis.yml` 测试挂载真实三包组合。ACP 与 headless 快照通过 opt-in overlay 固定 6 个 schema、有界结果和错误;TUI 快照固定 terminal 与 generic 卡片展示。 -- 包契约、架构图、核心数据结构、生成目录和 website API 描述同一个已发布接口。 +- 包契约、架构图、子系统页面、生成目录和 website API 描述同一个已发布接口。 - 仓库 CI 等价序列负责类型、lint、覆盖率、快照、文档、构建、hygiene、demo 和 built-entry 验证。 ## 后果 diff --git a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.i18n.yaml index c3bbbe1d22..e58d618cf3 100644 --- a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.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-19-plugin-command-registration.md -2026-07-19-plugin-command-registration.md: 5233ce511dc9798733513ccbf6824f3e1b68d2e6 -2026-07-19-plugin-command-registration.zh.md: 7f41b9d2d00373901b51e3338fa51e1cadf886a9 +2026-07-19-plugin-command-registration.md: c8f0f2772a41948e9eb257a16f40194518c568f9 +2026-07-19-plugin-command-registration.zh.md: 94853393f5f71b29cfbd8da5443b3d73ded20e92 diff --git a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md index 5233ce511d..c8f0f2772a 100644 --- a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md +++ b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md @@ -12,7 +12,7 @@ A shared mechanism must remain a UI concern rather than a model tool or agent-lo ## Decision -`@deepseek-ai/dsh-commands` in `packages/ui/commands/` is the product command registry. The TUI app bundle mounts it beside its consuming front door; the [automation-only ACP app](../simplification/2026-07-23-acp-automation-only-protocol.md) and the executor-less, UI-less agent spine omit it. TUI injects the service, while command producers depend only on the registry and any domain they operate. +`@deepseek-ai/dsh-commands` in `packages/interaction/commands/` is the product command registry. The TUI app bundle mounts it beside its consuming front door; the [automation-only ACP app](../simplification/2026-07-23-acp-automation-only-protocol.md) and the executor-less, UI-less agent spine omit it. TUI injects the service, while command producers depend only on the registry and any domain they operate. ### Registry contract diff --git a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md index 7f41b9d2d0..94853393f5 100644 --- a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md +++ b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md @@ -12,7 +12,7 @@ TUI 拥有斜杠命令。如果命令名、帮助文本、自动补全、分派 ## 决策 -位于 `packages/ui/commands/` 的 `@deepseek-ai/dsh-commands` 是产品命令注册表。TUI 应用 bundle(组合包)把它挂载在消费该服务的入口旁;[仅面向自动化的 ACP(Agent Client Protocol)应用](../simplification/2026-07-23-acp-automation-only-protocol.md)和无执行器、无 UI 的智能体 spine(主干)都省略该服务。TUI 注入该服务,命令生产者只依赖注册表及其操作的领域。 +位于 `packages/interaction/commands/` 的 `@deepseek-ai/dsh-commands` 是产品命令注册表。TUI 应用 bundle(组合包)把它挂载在消费该服务的入口旁;[仅面向自动化的 ACP(Agent Client Protocol)应用](../simplification/2026-07-23-acp-automation-only-protocol.md)和无执行器、无 UI 的智能体 spine(主干)都省略该服务。TUI 注入该服务,命令生产者只依赖注册表及其操作的领域。 ### 注册表契约 diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml index ee99911ee8..0cf926921b 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.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-20-dsh-cli-personal-config.md -2026-07-20-dsh-cli-personal-config.md: 2a8ae4b235823b4493d2f082d37b85806f45b662 -2026-07-20-dsh-cli-personal-config.zh.md: d8ff6c4fcc5da8f1db6f030e990118e30ae6fe41 +2026-07-20-dsh-cli-personal-config.md: c00432ba2b75cc42caf23e405c24acb33d839e7a +2026-07-20-dsh-cli-personal-config.zh.md: b33a7fa321ca04f41e47386544ffc0f268a8b1b6 diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md index 2a8ae4b235..c00432ba2b 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md @@ -48,4 +48,4 @@ The TUI and Web register the exact personal path through Cordis HMR after boot. ## Testing -`packages/ui/app-boot/tests/user-patches.spec.ts` pins parsing, startup application, exact-path add/failure/recovery/removal, last-good rollback, failure broadcast, and preservation of app-owned patches. `apps/cli/tests/built-bin.e2e.ts` boots the real dsh bin over a profile and exercises the live patch layer end to end. Test launchers isolate `$DSH_HOME`, so a developer's real overlay cannot leak into fixtures. +`packages/boot/app-boot/tests/user-patches.spec.ts` pins parsing, startup application, exact-path add/failure/recovery/removal, last-good rollback, failure broadcast, and preservation of app-owned patches. `apps/cli/tests/built-bin.e2e.ts` boots the real dsh bin over a profile and exercises the live patch layer end to end. Test launchers isolate `$DSH_HOME`, so a developer's real overlay cannot leak into fixtures. diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md index d8ff6c4fcc..b33a7fa321 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md @@ -48,4 +48,4 @@ TUI 和 Web 启动后通过 Cordis HMR(热模块替换)注册确切的个人 ## Testing -`packages/ui/app-boot/tests/user-patches.spec.ts` 固定解析、启动时应用、确切路径的新增/失败/恢复/移除、最后可用状态回滚、失败广播以及应用自有 patch 的保留。`apps/cli/tests/built-bin.e2e.ts` 启动真实 dsh bin 并基于 profile 端到端验证实时 patch 层。测试启动器会隔离 `$DSH_HOME`,因此开发者的真实 overlay 不会泄漏进 fixture。 +`packages/boot/app-boot/tests/user-patches.spec.ts` 固定解析、启动时应用、确切路径的新增/失败/恢复/移除、最后可用状态回滚、失败广播以及应用自有 patch 的保留。`apps/cli/tests/built-bin.e2e.ts` 启动真实 dsh bin 并基于 profile 端到端验证实时 patch 层。测试启动器会隔离 `$DSH_HOME`,因此开发者的真实 overlay 不会泄漏进 fixture。 diff --git a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml index a7d065d42d..1d93b7f440 100644 --- a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.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-21-continuable-background-subagents.md -2026-07-21-continuable-background-subagents.md: 6bbbfb25fb24eba95de888c9d23c79e24fc8535b -2026-07-21-continuable-background-subagents.zh.md: e23fbe67cc2d16a2c01a1b7ed4884008f8932dd4 +2026-07-21-continuable-background-subagents.md: e37abdd798242bc1086754e86a85ef46c08e59a3 +2026-07-21-continuable-background-subagents.zh.md: 799e1914bdeb571dc697652081be6b0170768655 diff --git a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md index 6bbbfb25fb..e37abdd798 100644 --- a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md +++ b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md @@ -112,7 +112,7 @@ Task records and active-run associations are process-local. Persistence makes th - `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` pins the continuable durability boundary: an absent or detached flush listener and a permanent listener failure reject with `DURABILITY_FAILED`, a transient loop-checkpoint failure can succeed on the final confirmation, cancellation owns either final-checkpoint outcome, resume also confirms durability, and foreground runs remain best-effort. `packages/subagent/subagent/tests/continuation.spec.ts` drives the real stack (agent loop, JSONL persistence, spawn/fork providers, Task service, and `ctx.subagents`) keylessly: initial and resumed activations create fresh Tasks and dispose their runs before terminal; the descriptor event is pre-turn, model-hidden, versioned, durable under the service-allocated child id, and survives blocked or throwing initial prompt admission; cancellation, steering, cold follow-up, authorization, ownership conflicts, and resume races retain the contracts described above. - `packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts` pins the `send_message` schema, coordinator attribution, both route renderings, the not-delivered failure, the no-agent rejection, and HMR disposal. - `packages/subagent/tool-subagent/tests/tool-subagent.spec.ts` covers the configured background route: continuable mode requires a resumable provider and returns both ids without requiring `send_message`, while one-shot mode keeps the plain Task acknowledgement even when the provider can resume. -- `packages/sdk/helper/tests/project.spec.ts` pins the Task service and model-facing Task controls in generated spawn and fork compositions. +- `packages/scaffold/helper/tests/project.spec.ts` pins the Task service and model-facing Task controls in generated spawn and fork compositions. - The keyless ACP snapshot scenario `subagent-continuable` (examples/acp-agent) pins the model-visible transcript: the two-id acknowledgement, a final durability-confirmation failure rendered through `task_output` without unconfirmed child output, and a `send_message` follow-up whose started Task fails with the id unavailable. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md index e23fbe67cc..799e1914bd 100644 --- a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md +++ b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md @@ -112,7 +112,7 @@ Task 记录和活跃 run 关联都位于进程内。持久化使 child 会话可 - `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` 固定可继续执行的持久性边界:缺少 flush 监听器、flush 监听器已脱离或监听器持续失败时,均会以 `DURABILITY_FAILED` 拒绝;循环检查点的瞬时失败可在最终确认成功后继续完成,发生取消时最终检查点无论成功还是失败都由取消优先决定结果,resume 同样会确认持久性,而前台运行仍采用尽力而为策略。`packages/subagent/subagent/tests/continuation.spec.ts` 以无密钥方式驱动真实栈(agent loop、JSONL 持久化、spawn/fork 提供方、Task 服务和 `ctx.subagents`):初始及恢复后的激活都会创建新 Task,并在进入终态前 dispose 各自的 run;描述符事件位于轮次前、对模型隐藏、带版本、在服务分配的 child id 下持久化,并在初始 prompt admission 阻止请求或抛出异常时仍保留;取消、steering、cold follow-up、授权、所有权冲突与 resume 竞态保留上述契约。 - `packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts` 固定 `send_message` 的 schema、coordinator 来源标记、两种路由渲染、未送达失败、无 agent 时的拒绝,以及 HMR(热模块替换)dispose。 - `packages/subagent/tool-subagent/tests/tool-subagent.spec.ts` 覆盖配置的后台路由:可继续模式要求提供方可恢复,并在不要求 `send_message` 的情况下返回两个 id;即使提供方可以恢复,一次性模式仍保持普通的 Task 确认消息。 -- `packages/sdk/helper/tests/project.spec.ts` 固定生成的 spawn 与 fork 组合中的 Task 服务及面向模型的 Task 控制工具。 +- `packages/scaffold/helper/tests/project.spec.ts` 固定生成的 spawn 与 fork 组合中的 Task 服务及面向模型的 Task 控制工具。 - 无密钥 ACP 快照场景 `subagent-continuable`(examples/acp-agent)固定模型可见的 transcript:双 id 确认消息、最终持久性确认失败(该失败通过 `task_output` 呈现,且不包含未经确认的 child 输出),以及一次 `send_message` 后续操作——其已启动的 Task 会带着「id 不可用」失败。 ## 影响 diff --git a/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.i18n.yaml b/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.i18n.yaml index 5c5054b0c9..e109560a2f 100644 --- a/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.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-21-log-backed-session-titles.md -2026-07-21-log-backed-session-titles.md: 5f4c53a5fb866663bbfaae6b3fdafb4660424804 -2026-07-21-log-backed-session-titles.zh.md: f9629c1109c8428c74270de9475423d29b99e8f1 +2026-07-21-log-backed-session-titles.md: 89e6e67fb9ece4c087ba7b1bc52e61b78b94586c +2026-07-21-log-backed-session-titles.zh.md: 4df21be565f57db056f506df477f665908d1fdfa diff --git a/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md b/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md index 5f4c53a5fb..89e6e67fb9 100644 --- a/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md +++ b/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md @@ -12,7 +12,7 @@ Session identity metadata is immutable, and the event log is the replay and fork ## Decision -The [`session-title` capability family](../../../../packages/session-title/README.md) owns title state and generation policy. `@deepseek-ai/dsh-session-title` provides `ctx.sessionTitle`, a deterministic first-message fallback, and a registry for at most one optional asynchronous provider. `@deepseek-ai/dsh-session-title-llm` owns the common auxiliary-model request policy; separate first-message and all-user-messages plugins choose input cadence. The shared agent spine mounts only the fallback service. The Web host mounts that service plus the first-message model provider with explicit overridable limits, so a fresh Web session gains an immediate fallback and then a non-blocking model summary. Other compositions choose either model provider explicitly. +The [`session-title` capability family](../../../../packages/session/README.md) owns title state and generation policy. `@deepseek-ai/dsh-session-title` provides `ctx.sessionTitle`, a deterministic first-message fallback, and a registry for at most one optional asynchronous provider. `@deepseek-ai/dsh-session-title-llm` owns the common auxiliary-model request policy; separate first-message and all-user-messages plugins choose input cadence. The shared agent spine mounts only the fallback service. The Web host mounts that service plus the first-message model provider with explicit overridable limits, so a fresh Web session gains an immediate fallback and then a non-blocking model summary. Other compositions choose either model provider explicitly. ### Event ownership and folding diff --git a/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.zh.md b/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.zh.md index f9629c1109..4df21be565 100644 --- a/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.zh.md +++ b/.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -[`session-title` 功能包族](../../../../packages/session-title/README.md)负责标题状态和生成策略。`@deepseek-ai/dsh-session-title` 提供 `ctx.sessionTitle`、确定性的首消息回退方案,以及一个至多接受单个可选异步提供方的注册表。`@deepseek-ai/dsh-session-title-llm` 负责通用的辅助模型请求策略;首消息插件和全部用户消息插件分别选择输入调度方式。共享 agent 主干只挂载回退服务。Web host 会挂载该服务和首消息模型提供方,并显式设置可覆盖的限制,因此新建的 Web 会话会立即获得回退标题,随后在不阻塞主响应的情况下获得模型摘要。其他组合需显式选择任一模型提供方。 +[`session-title` 功能包族](../../../../packages/session/README.md)负责标题状态和生成策略。`@deepseek-ai/dsh-session-title` 提供 `ctx.sessionTitle`、确定性的首消息回退方案,以及一个至多接受单个可选异步提供方的注册表。`@deepseek-ai/dsh-session-title-llm` 负责通用的辅助模型请求策略;首消息插件和全部用户消息插件分别选择输入调度方式。共享 agent 主干只挂载回退服务。Web host 会挂载该服务和首消息模型提供方,并显式设置可覆盖的限制,因此新建的 Web 会话会立即获得回退标题,随后在不阻塞主响应的情况下获得模型摘要。其他组合需显式选择任一模型提供方。 ### 事件归属与折叠 diff --git a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml index 27786e85a2..58b5371ba2 100644 --- a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.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-23-session-telemetry-otel-revival.md -2026-07-23-session-telemetry-otel-revival.md: f83128e8bf62e0718e59912c16c4e449855aaa1a -2026-07-23-session-telemetry-otel-revival.zh.md: bef3bd9cbc22f363b92880864651dcaa782fc1de +2026-07-23-session-telemetry-otel-revival.md: e6a213af65451a2ae26a5eb685c1a0f6ab220869 +2026-07-23-session-telemetry-otel-revival.zh.md: 4e834f8b129e2062064cf3497370fecf6f88db29 diff --git a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md index f83128e8bf..e6a213af65 100644 --- a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md +++ b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md @@ -10,7 +10,7 @@ Every deployment that wants harness sessions in an observability stack must hand ## Decision -`packages/telemetry/` revives the two reviewed packages under the SDK stance — the harness provides the capability, the deployment configures where records go and owns what leaves in them: +`packages/session/` (formerly `telemetry/`) revives the two reviewed packages under the SDK stance — the harness provides the capability, the deployment configures where records go and owns what leaves in them: - **`@deepseek-ai/dsh-session-telemetry`** — the seam. `TelemetryBackend` (`emit`/`flush?`/`shutdown`), the service-registered `Telemetry` form, and `TelemetryCoordinator` owning capture: live adoption with cursor read-back and the per-append firehose (project → `structuredClone` → redact → `emit`, zero I/O), buffer-free on-demand replay from the canonical log, the fixed first-chunk-per-(turn, step) projection, the live `agent/error` relay, and live dispose-time `shutdown` records. - **The `telemetry/record` waterfall** — the delta over the branch version and the seam's redaction extension point. Every record passes it before reaching any backend; the seam ships NO rules of its own — the innermost `next()` is a pass-through, deployments mount their rules as listeners (stacking by transforming `next()`'s return value), and a throwing rule withholds the record fail-closed. Redaction applies to the exported copy only; the canonical log is never rewritten. diff --git a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md index bef3bd9cbc..4e834f8b12 100644 --- a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md +++ b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -`packages/telemetry/` 以 SDK 立场复活这两个经过评审的包——harness 提供能力,部署方配置上报去向并对导出内容负责: +`packages/session/`(原 `telemetry/`)以 SDK 立场复活这两个经过评审的包——harness 提供能力,部署方配置上报去向并对导出内容负责: - **`@deepseek-ai/dsh-session-telemetry`** —— seam 本体。`TelemetryBackend`(`emit`/`flush?`/`shutdown`)、服务注册形态的 `Telemetry`、以及拥有捕获侧的 `TelemetryCoordinator`:带游标回读的实时收养与逐 append 的 firehose(投影 → `structuredClone` → 脱敏 → `emit`,零 I/O)、从权威日志进行的无缓冲按需回放、固定的每个(轮次、步骤)组合首分片投影、实时 `agent/error` 转发,以及实时 dispose(资源释放)时的 `shutdown` 记录。 - **`telemetry/record` waterfall(瀑布式事件)** —— 相对分支版本的增量,也是该 seam 的脱敏扩展点。每条记录抵达任何后端前必经此处;seam 自身不带任何规则——最内层 `next()` 原样透传,部署方以监听器挂载自己的规则(通过变换 `next()` 的返回值堆叠),抛异常的规则将该记录 fail-closed 扣下。脱敏只作用于导出副本;canonical log 永不改写。 diff --git a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml index 6018cb1305..38dc165e7f 100644 --- a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.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-27-typescript-sdk-and-sdk-subagent-backend.md -2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: 96ffd772810a7908ff452967aa0be0e540bc2d8c -2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 74ae4b81807d1267dc19f30d2bb51e50a775c4be +2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: c5c6c4eac053e9a3adfa2a2a441ac492ba3391ac +2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 643ec36134a1ebe940da5de5b373fa0c25afcd42 diff --git a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md index 96ffd77281..c5c6c4eac0 100644 --- a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md +++ b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md @@ -12,8 +12,8 @@ The stdio JSON-RPC serving surface (`@deepseek-ai/dsh-jsonrpc`, the [single-exe Three packages, layered exactly like the existing Python stack, plus one seam registration: -- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/sdk/sdk-protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-jsonrpc` (which now imports it), and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. The package root explicitly exports that complete interface and provides no source-module deep imports. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. One behavioral change: an error response now rejects with `JsonRpcResponseError` carrying the wire `code`/`data` (the Python client already preserved these; the old transport threw a bare `Error` with only the message). -- **`@deepseek-ai/dsh-sdk-client`** (`packages/sdk/sdk-client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Its package-root consumer interface explicitly exports both client layers, caller-facing types, and the protocol-owned `JsonRpcResponseError`; source modules, normalization helpers, and the notification producer stay internal. `TurnResult.events` contains only the root session's typed events, while `notifications` retains session ids across the root and descendants discovered from `subagent.started`; session-tree scoping is client-side, mirroring `client.py`. Deliberate asymmetries with Python: the launch spec is explicit `command`/`args` (no bundled-runtime resolution — that is a distribution concern with no TS consumer yet); `env` replaces rather than merges (callers own credential policy; `scrubbedParentEnv` from the subprocess seam is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit (the client runs outside any harness context, so it cannot ride `ctx.subprocess`). +- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/scaffold/protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-jsonrpc` (which now imports it), and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. The package root explicitly exports that complete interface and provides no source-module deep imports. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. One behavioral change: an error response now rejects with `JsonRpcResponseError` carrying the wire `code`/`data` (the Python client already preserved these; the old transport threw a bare `Error` with only the message). +- **`@deepseek-ai/dsh-sdk-client`** (`packages/scaffold/client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Its package-root consumer interface explicitly exports both client layers, caller-facing types, and the protocol-owned `JsonRpcResponseError`; source modules, normalization helpers, and the notification producer stay internal. `TurnResult.events` contains only the root session's typed events, while `notifications` retains session ids across the root and descendants discovered from `subagent.started`; session-tree scoping is client-side, mirroring `client.py`. Deliberate asymmetries with Python: the launch spec is explicit `command`/`args` (no bundled-runtime resolution — that is a distribution concern with no TS consumer yet); `env` replaces rather than merges (callers own credential policy; `scrubbedParentEnv` from the subprocess seam is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit (the client runs outside any harness context, so it cannot ride `ctx.subprocess`). - **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Its `provider`/`model` config feeds the child's `initialize`; `env` is where deployments pass the child's own key and `DSH_CORDIS_CONFIG`. - **The subagent seam grows `out-of-process.ts`**: the provider-side vocabulary both out-of-process backends share — `NO_START_CAPABILITIES`, timing-bound validation, child cwd resolution (config override, else the delegating parent session's workspace), the never-reject `settleRunResult`, and the `subprocessRunHandle` publication. Process mechanics (spawn, env scrub, tree-scoped teardown) live in the `dsh-subprocess` seam; `subagent-acp` spawns through `ctx.subprocess`, while this backend spawns through the SDK client (the subprocess README's documented exception for SDK-managed transports) and applies the seam's `scrubbedParentEnv()` itself. diff --git a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md index 74ae4b8180..643ec36134 100644 --- a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md +++ b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md @@ -12,8 +12,8 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-jsonrpc`,见[单文件 三个包,分层与既有 Python 栈完全一致,外加一个 seam 注册: -- **`@deepseek-ai/dsh-sdk-protocol`**(`packages/sdk/sdk-protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport` 从 `dsh-jsonrpc` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result`、`SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。该包根显式导出这一完整接口,且不提供指向源模块的深层导入。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data` 的 `JsonRpcResponseError` 拒绝(Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。 -- **`@deepseek-ai/dsh-sdk-client`**(`packages/sdk/sdk-client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(spawn、分帧、通知扇出、有类型的错误表面、经共享 dispose(资源释放)阶梯关闭至完全停稳)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize`、`run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。其包根消费方接口显式导出两层客户端、面向调用方的类型,以及协议包所拥有的 `JsonRpcResponseError`;源模块、规范化辅助函数和通知投递端都保留为内部实现。`TurnResult.events` 只包含根会话的类型化事件,而 `notifications` 则保留根会话及从 `subagent.started` 发现的后代各自的会话 id;基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费方的发行问题);`env` 整体替换而非合并(凭据策略归调用方;subprocess seam 的 `scrubbedParentEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`(Python 只暴露 `status`);拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出(客户端运行在任何 harness 上下文之外,无法搭乘 `ctx.subprocess`)。 +- **`@deepseek-ai/dsh-sdk-protocol`**(`packages/scaffold/protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport` 从 `dsh-jsonrpc` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result`、`SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。该包根显式导出这一完整接口,且不提供指向源模块的深层导入。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data` 的 `JsonRpcResponseError` 拒绝(Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。 +- **`@deepseek-ai/dsh-sdk-client`**(`packages/scaffold/client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(spawn、分帧、通知扇出、有类型的错误表面、经共享 dispose(资源释放)阶梯关闭至完全停稳)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize`、`run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。其包根消费方接口显式导出两层客户端、面向调用方的类型,以及协议包所拥有的 `JsonRpcResponseError`;源模块、规范化辅助函数和通知投递端都保留为内部实现。`TurnResult.events` 只包含根会话的类型化事件,而 `notifications` 则保留根会话及从 `subagent.started` 发现的后代各自的会话 id;基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费方的发行问题);`env` 整体替换而非合并(凭据策略归调用方;subprocess seam 的 `scrubbedParentEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`(Python 只暴露 `status`);拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出(客户端运行在任何 harness 上下文之外,无法搭乘 `ctx.subprocess`)。 - **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,采用与 `subagent-acp` 对等的结构:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样通过 `onError` sink 将结果归一为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未运行任何轮次便已结束的子进程,都是 `error`)。其 `provider`/`model` 配置喂给子进程的 `initialize`;`env` 是部署传入子进程自有密钥与 `DSH_CORDIS_CONFIG` 的地方。 - **subagent seam 新增 `out-of-process.ts`**:两个进程外后端共享的 provider 侧词汇——`NO_START_CAPABILITIES`、时限校验、子进程 cwd 解析(配置覆盖、否则发起委托的父会话工作区)、绝不拒绝的 `settleRunResult`、以及 `subprocessRunHandle` 发布。进程机制(spawn、环境清理、进程树清理)属于 `dsh-subprocess` seam;`subagent-acp` 经 `ctx.subprocess` spawn 子进程,本后端则经 SDK 客户端 spawn 子进程(subprocess README 记载的 SDK 托管传输例外)并自行应用该 seam 的 `scrubbedParentEnv()`。 diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml index 8374a59b63..9f60321cc8 100644 --- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.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-28-feedback-command.md -2026-07-28-feedback-command.md: 56660b3796c44d3d510dd2901e6a155498673d88 -2026-07-28-feedback-command.zh.md: ec03a5c4a0e96d5350085c3d8d26f2523962f1b5 +2026-07-28-feedback-command.md: 78cc9e89a5811b3f1520bae2bd971cbcf2522ede +2026-07-28-feedback-command.zh.md: c8870ca2988501d7899ae826e1bbcbad0bef5678 diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.md b/.agents/notes/implemented/feature/2026-07-28-feedback-command.md index 56660b3796..78cc9e89a5 100644 --- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.md +++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.md @@ -46,7 +46,7 @@ Surrounding whitespace is discarded, but nothing else is parsed. `/feedback /pla **Make `/feedback` a true no-op that records nothing.** The most literal reading of "does not do anything". Rejected because it makes the command pointless: the stated requirement was that the remark reach the session log. -**Register the command inside an existing package** such as `packages/ui/commands`. Avoids a new group and its README pair. Rejected: `ctx.commands` is the registry, not a home for arbitrary command implementations, and the requester asked for a standalone package. +**Register the command inside an existing package** such as `packages/interaction/commands`. Avoids a new group and its README pair. Rejected: `ctx.commands` is the registry, not a home for arbitrary command implementations, and the requester asked for a standalone package. **Parse structure out of the text** (category prefixes, severity markers). Rejected as speculative: no consumer needs that structure, and any control-word grammar makes the corresponding literal feedback unrecordable. Verbatim text is the widest surface a future consumer can narrow; a parsed one cannot be widened after the fact. diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md b/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md index ec03a5c4a0..c8870ca298 100644 --- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md @@ -46,7 +46,7 @@ Status: implemented **让 `/feedback` 成为真正的空操作,什么都不记录。** 这是对「什么都不做」最字面的理解。已否决:这会使命令失去意义——明确的要求是让这条评论进入会话日志。 -**在现有包中注册该命令**,例如 `packages/ui/commands`。可省去新分组及其双语 README。已否决:`ctx.commands` 是注册表,而不是任意命令实现的归属地;且请求者明确要求独立的包。 +**在现有包中注册该命令**,例如 `packages/interaction/commands`。可省去新分组及其双语 README。已否决:`ctx.commands` 是注册表,而不是任意命令实现的归属地;且请求者明确要求独立的包。 **从文本中解析结构**(类别前缀、严重程度标记)。已否决,属于投机设计:没有消费方需要该结构,而任何控制词语法都会让对应的字面反馈无法记录。原样文本是未来消费方可以收窄的最宽接口;而已被解析的接口无法事后放宽。 diff --git a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml index 66ac99d37c..e6ca4048fd 100644 --- a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.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-31-code-mode-language-dispatch.md -2026-07-31-code-mode-language-dispatch.md: 96001252d6494d058a8df9974fb5a0d59e7d7112 -2026-07-31-code-mode-language-dispatch.zh.md: aa7eb2a6b4b9117f1d707b37afcdbe12b814bad2 +2026-07-31-code-mode-language-dispatch.md: 71e4fd5a30e90ac66132dbdcea16917bacc80b68 +2026-07-31-code-mode-language-dispatch.zh.md: 1dbb3d889d28669575980d3a667e4f168a5d72db diff --git a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md index 96001252d6..71e4fd5a30 100644 --- a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md +++ b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md @@ -17,7 +17,7 @@ Language selection is a lookup on `ctx.codeRuntime.language`, resolved lazily at - `SDK_RENDERERS` (index.ts) maps a language to its `tools:sdk` renderer — `typescript → renderToolsSdk`, `python → renderToolsSdkPy`. The `tools:sdk` section reads the loaded runtime's language and picks the renderer; `requireCodeRuntime` rejects a `mode: code`/`both` runtime whose language is absent from the table, naming the known languages. - `RUN_CODE_FLAVORS` (code-mode.ts) maps a language to its two model-facing `run_code` strings (tool `description` and the `code` parameter description), so a language's SDK section and its transport schema always agree. -Both tables are read with `Object.hasOwn` before use so a language named `toString`/`constructor` cannot resolve an inherited `Object.prototype` member as a renderer. The two guards differ in reachability: `SDK_RENDERERS`' in-callback guard is unreachable because `requireCodeRuntime` validated the same `const` table earlier in the same callback (it carries a `/* v8 ignore */`), while `RUN_CODE_FLAVORS`' guard is the primary, publicly reachable rejection — any language absent from the flavor table hits it through `run_code`'s language-aware getters, which the public `schemas()` reaches without passing `requireCodeRuntime` first; the test reads one of those getters off the definition directly, under a language absent from both tables. A language present in `SDK_RENDERERS` but not `RUN_CODE_FLAVORS` is drift the shared `CodeSdkLanguage` `satisfies` pins reject at `typecheck`, so it is not an input either guard can see; what the guards still own is a mounted runtime reporting a language absent from both tables. Schema emission reads the runtime through `peekRuntime()` rather than `requireRuntime()`: `undefined` (no runtime mounted, reached by definition readers and `schemas()`, of which the doc-catalog harvest is the only shipped one and none of which feeds a model because assembly passes `requireCodeRuntime` first) degrades to the TypeScript flavor, whereas a mounted unknown language fails loud — this is NOT the silent fallback rejected below, which concerns emitting a wrong-language SDK for a real runtime. Adding a backend language is three parallel edits — a `CodeSdkLanguage` member and the two table entries — plus its renderer and the prose that names the well-known values instead of deriving them (the seam's `dsh-code-runtime` README pair, its `CodeRuntime.language` JSDoc, and the `docs/core-data-structures/code-runtime.md` pair; this package's own README pair and its `Config.mode` JSDoc — no gate checks any of it), with no `agent-loop` or registry-structure change. +Both tables are read with `Object.hasOwn` before use so a language named `toString`/`constructor` cannot resolve an inherited `Object.prototype` member as a renderer. The two guards differ in reachability: `SDK_RENDERERS`' in-callback guard is unreachable because `requireCodeRuntime` validated the same `const` table earlier in the same callback (it carries a `/* v8 ignore */`), while `RUN_CODE_FLAVORS`' guard is the primary, publicly reachable rejection — any language absent from the flavor table hits it through `run_code`'s language-aware getters, which the public `schemas()` reaches without passing `requireCodeRuntime` first; the test reads one of those getters off the definition directly, under a language absent from both tables. A language present in `SDK_RENDERERS` but not `RUN_CODE_FLAVORS` is drift the shared `CodeSdkLanguage` `satisfies` pins reject at `typecheck`, so it is not an input either guard can see; what the guards still own is a mounted runtime reporting a language absent from both tables. Schema emission reads the runtime through `peekRuntime()` rather than `requireRuntime()`: `undefined` (no runtime mounted, reached by definition readers and `schemas()`, of which the doc-catalog harvest is the only shipped one and none of which feeds a model because assembly passes `requireCodeRuntime` first) degrades to the TypeScript flavor, whereas a mounted unknown language fails loud — this is NOT the silent fallback rejected below, which concerns emitting a wrong-language SDK for a real runtime. Adding a backend language is three parallel edits — a `CodeSdkLanguage` member and the two table entries — plus its renderer and the prose that names the well-known values instead of deriving them (the seam's `dsh-code-runtime` README pair, its `CodeRuntime.language` JSDoc, and the `docs/subsystems/code-runtime.md` pair; this package's own README pair and its `Config.mode` JSDoc — no gate checks any of it), with no `agent-loop` or registry-structure change. `code-mode.ts` depends only on the runtime seam (`@deepseek-ai/dsh-code-runtime`), never on a concrete backend; dispatch is by `runtime.language` at run time. The tool layer therefore lands independently of the protocol and backend PRs — it needs only the seam's `language` field, which is already on master. @@ -37,7 +37,7 @@ The standard that cap serves is grammatical validity, and the boundary is delibe ## Consequences -Adding a backend language is three parallel edits — a `CodeSdkLanguage` member, an `SDK_RENDERERS` entry, and a `RUN_CODE_FLAVORS` entry — plus the renderer function the second points at, with no change to `agent-loop` or the registry structure. The two tables (`SDK_RENDERERS`, `RUN_CODE_FLAVORS`) must stay in step, and that invariant is checked statically rather than left to review: both are `satisfies`-checked against that one union, so a language added to one and not the other fails `typecheck`. This is the mechanical form the drift risk deserves — the runtime `Object.hasOwn` guards would catch it too, but only once a backend reporting that language ships: one PR after the drift, at the consumer's integration point rather than where it was introduced, and on this base never, since no second backend exists. The tables keep their `Record` declared type because `CodeRuntime.language` is an unconstrained `string`; the union pins what the harness ships, the guards reject what a runtime reports. What stays outside that check is the prose that names the well-known values instead of deriving them: `dsh-code-runtime`'s README pair, its `CodeRuntime.language` JSDoc, and the `docs/core-data-structures/code-runtime.md` pair at the seam, plus this package's own README pair and its `Config.mode` JSDoc. Earlier notes name the values as the state at their own PR and are not on that list. Two separate reasons keep it ungated. Prose is not type-checked at all, wherever the union lives. And no type-level pin can stand in for it here: the interface package must not import its consumer's table, and `CodeRuntime.language` stays an unconstrained `string` by design, so moving the union into the seam would not apply it either. A unit test pinning the two key sets equal was rejected in favor of this: it would buy the same check at the cost of a test-only export of two private tables, and would run later than the compiler does. Which of the two runtime failures surfaces depends on the entry point, for a language absent from both tables: assembly reports the missing renderer, because `wireSchemas` calls `requireCodeRuntime` before projecting, while the public `schemas()` reaches `run_code`'s language-aware getters first and reports the missing flavor. The tool layer stays free of any concrete backend dependency, so it lands and is testable on master ahead of the Python protocol and backend. +Adding a backend language is three parallel edits — a `CodeSdkLanguage` member, an `SDK_RENDERERS` entry, and a `RUN_CODE_FLAVORS` entry — plus the renderer function the second points at, with no change to `agent-loop` or the registry structure. The two tables (`SDK_RENDERERS`, `RUN_CODE_FLAVORS`) must stay in step, and that invariant is checked statically rather than left to review: both are `satisfies`-checked against that one union, so a language added to one and not the other fails `typecheck`. This is the mechanical form the drift risk deserves — the runtime `Object.hasOwn` guards would catch it too, but only once a backend reporting that language ships: one PR after the drift, at the consumer's integration point rather than where it was introduced, and on this base never, since no second backend exists. The tables keep their `Record` declared type because `CodeRuntime.language` is an unconstrained `string`; the union pins what the harness ships, the guards reject what a runtime reports. What stays outside that check is the prose that names the well-known values instead of deriving them: `dsh-code-runtime`'s README pair, its `CodeRuntime.language` JSDoc, and the `docs/subsystems/code-runtime.md` pair at the seam, plus this package's own README pair and its `Config.mode` JSDoc. Earlier notes name the values as the state at their own PR and are not on that list. Two separate reasons keep it ungated. Prose is not type-checked at all, wherever the union lives. And no type-level pin can stand in for it here: the interface package must not import its consumer's table, and `CodeRuntime.language` stays an unconstrained `string` by design, so moving the union into the seam would not apply it either. A unit test pinning the two key sets equal was rejected in favor of this: it would buy the same check at the cost of a test-only export of two private tables, and would run later than the compiler does. Which of the two runtime failures surfaces depends on the entry point, for a language absent from both tables: assembly reports the missing renderer, because `wireSchemas` calls `requireCodeRuntime` before projecting, while the public `schemas()` reaches `run_code`'s language-aware getters first and reports the missing flavor. The tool layer stays free of any concrete backend dependency, so it lands and is testable on master ahead of the Python protocol and backend. The cost is that the Python branch of both tables is unreachable on this base: `CodeRuntime.language` is set by the loaded backend, the only published backend is `dsh-code-runtime-worker` (`'typescript'`), and the registry reads the loaded runtime rather than a config field, so no assembled application can select `renderToolsSdkPy` or `PYTHON_FLAVOR`. The model-visible surface is therefore unchanged by this note's work until a backend reporting `'python'` is published, and this PR's coverage is unit-level — the renderer output plus the dispatch and rejection paths. The keyless snapshot for the Python model interface belongs to the PR that publishes that backend, because only there does a real `cordis.yml` over published plugins produce a Python assembly; a snapshot example that mounted a fixture runtime here would assert against a test double, which [docs/testing.md](../../../../docs/testing.md) rejects as a substitute for the assembled application transcript. diff --git a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md index aa7eb2a6b4..1dbb3d889d 100644 --- a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md @@ -17,7 +17,7 @@ Code Mode 只生成一种 SDK 形态:TypeScript。`ToolRegistry` 为 `tools:sd - `SDK_RENDERERS`(index.ts)把语言映射到它的 `tools:sdk` 渲染器——`typescript → renderToolsSdk`、`python → renderToolsSdkPy`。`tools:sdk` 段读取所加载运行时的语言并选出渲染器;`requireCodeRuntime` 拒绝其语言不在表中的 `mode: code`/`both` 运行时,并列出已知语言。 - `RUN_CODE_FLAVORS`(code-mode.ts)把语言映射到它那两条面向模型的 `run_code` 字符串(工具 `description` 与 `code` 参数描述),使一种语言的 SDK 段与它的传输 schema 始终一致。 -两张表在使用前都以 `Object.hasOwn` 读取,这样名为 `toString`/`constructor` 的语言不会把继承自 `Object.prototype` 的成员解析成渲染器。两个守卫的可达性不同:`SDK_RENDERERS` 的段内守卫不可达,因为 `requireCodeRuntime` 已在同一回调更早处校验过同一张 `const` 表(它带 `/* v8 ignore */`);而 `RUN_CODE_FLAVORS` 的守卫是主要的、可公开到达的拒绝路径——任何缺席 flavor 表的语言都经 `run_code` 的语言感知 getter 到达它,而公共 `schemas()` 抵达那些 getter 时并未先过 `requireCodeRuntime`;测试直读 definition 上的其中一个 getter,用的是对两张表都缺席的语言。「在 `SDK_RENDERERS` 里却不在 `RUN_CODE_FLAVORS` 里」这种漂移已由共享的 `CodeSdkLanguage` `satisfies` 在 `typecheck` 处拒绝,两个守卫都看不到这种输入;它们如今负责的是所挂载运行时报告了一门两张表都缺席的语言。schema 发射通过 `peekRuntime()` 而非 `requireRuntime()` 读取运行时:`undefined`(无运行时,由直读 definition 的读者与 `schemas()` 到达,其中 doc-catalog 采集是唯一已交付的一个,而它们都不会喂给模型,因为组装路径先过 `requireCodeRuntime`)降级到 TypeScript flavor,而挂载了未知语言则 fail loud——这不是下方被否决的静默回退,那指的是为真实运行时发出错误语言的 SDK。新增一门后端语言是三处并列编辑——一个 `CodeSdkLanguage` 成员加两条表项——再加它的渲染器,以及点名已知值而非从中派生的散文(seam 侧的 `dsh-code-runtime` README 双语对、它的 `CodeRuntime.language` JSDoc 与 `docs/core-data-structures/code-runtime.md` 双语对;本包自己的 README 双语对与它的 `Config.mode` JSDoc,无任何 gate 检查其中任何一处),不动 `agent-loop`,也不动注册表结构。 +两张表在使用前都以 `Object.hasOwn` 读取,这样名为 `toString`/`constructor` 的语言不会把继承自 `Object.prototype` 的成员解析成渲染器。两个守卫的可达性不同:`SDK_RENDERERS` 的段内守卫不可达,因为 `requireCodeRuntime` 已在同一回调更早处校验过同一张 `const` 表(它带 `/* v8 ignore */`);而 `RUN_CODE_FLAVORS` 的守卫是主要的、可公开到达的拒绝路径——任何缺席 flavor 表的语言都经 `run_code` 的语言感知 getter 到达它,而公共 `schemas()` 抵达那些 getter 时并未先过 `requireCodeRuntime`;测试直读 definition 上的其中一个 getter,用的是对两张表都缺席的语言。「在 `SDK_RENDERERS` 里却不在 `RUN_CODE_FLAVORS` 里」这种漂移已由共享的 `CodeSdkLanguage` `satisfies` 在 `typecheck` 处拒绝,两个守卫都看不到这种输入;它们如今负责的是所挂载运行时报告了一门两张表都缺席的语言。schema 发射通过 `peekRuntime()` 而非 `requireRuntime()` 读取运行时:`undefined`(无运行时,由直读 definition 的读者与 `schemas()` 到达,其中 doc-catalog 采集是唯一已交付的一个,而它们都不会喂给模型,因为组装路径先过 `requireCodeRuntime`)降级到 TypeScript flavor,而挂载了未知语言则 fail loud——这不是下方被否决的静默回退,那指的是为真实运行时发出错误语言的 SDK。新增一门后端语言是三处并列编辑——一个 `CodeSdkLanguage` 成员加两条表项——再加它的渲染器,以及点名已知值而非从中派生的散文(seam 侧的 `dsh-code-runtime` README 双语对、它的 `CodeRuntime.language` JSDoc 与 `docs/subsystems/code-runtime.md` 双语对;本包自己的 README 双语对与它的 `Config.mode` JSDoc,无任何 gate 检查其中任何一处),不动 `agent-loop`,也不动注册表结构。 `code-mode.ts` 只依赖运行时 seam(`@deepseek-ai/dsh-code-runtime`),绝不依赖具体后端;分发在运行时按 `runtime.language` 进行。因此工具层独立于协议和后端 PR 落地——它只需要 seam 的 `language` 字段,而该字段已在 master 上。 @@ -37,7 +37,7 @@ Code Mode 只生成一种 SDK 形态:TypeScript。`ToolRegistry` 为 `tools:sd ## Consequences -新增一门后端语言是三处并列编辑——一个 `CodeSdkLanguage` 成员、一个 `SDK_RENDERERS` 表项、一个 `RUN_CODE_FLAVORS` 表项——再加第二处所指向的渲染器函数,不动 `agent-loop`,也不动注册表结构。两张表(`SDK_RENDERERS`、`RUN_CODE_FLAVORS`)必须同步,且这条不变式由静态检查把关,而非交给 review:两张表都以 `satisfies` 对上述同一个 union 校验,因此只加其一而漏掉另一会在 `typecheck` 处失败。这正是该漂移风险应有的机械形式——运行期的 `Object.hasOwn` 守卫同样能捕获,但要等到有后端报告该语言之后:晚于漂移引入一个 PR,且触发点在消费方的集成处而非漂移引入处;在当前 base 上则永远不会触发,因为不存在第二个后端。两张表的声明类型仍是 `Record`,因为 `CodeRuntime.language` 是不受约束的 `string`:union 钉住本仓库交付了什么,守卫拒绝运行时报告了什么。落在这条检查之外的是点名已知值而非从中派生的散文:seam 侧的 `dsh-code-runtime` README 双语对、它的 `CodeRuntime.language` JSDoc 与 `docs/core-data-structures/code-runtime.md` 双语对,再加本包自己的 README 双语对与它的 `Config.mode` JSDoc。更早的 note 点名这些值时记的是其自身 PR 当时的状态,不在此列。让它无 gate 的是两条独立理由。其一,散文根本不受类型检查,union 放在哪里都一样。其二,类型级替代在这里也不可用:接口包不得 import 其消费方的表,而 `CodeRuntime.language` 按设计保持不受约束的 `string`,即便把 union 迁进 seam 也不会作用到它。用一个断言两张表键集相等的 unit test 的方案被否决:它买到的是同一条检查,代价却是把两张私有表做测试专用导出,且运行时机晚于编译器。对两张表都缺席的语言,两种运行期失败中报出哪一条随入口而异:组装路径报缺渲染器,因为 `wireSchemas` 在投影前先调 `requireCodeRuntime`;而公共 `schemas()` 先经过 `run_code` 的语言感知 getter,报的是缺 flavor 表项。工具层不依赖任何具体后端,因此它能先于 Python 协议和后端在 master 上落地并可测。 +新增一门后端语言是三处并列编辑——一个 `CodeSdkLanguage` 成员、一个 `SDK_RENDERERS` 表项、一个 `RUN_CODE_FLAVORS` 表项——再加第二处所指向的渲染器函数,不动 `agent-loop`,也不动注册表结构。两张表(`SDK_RENDERERS`、`RUN_CODE_FLAVORS`)必须同步,且这条不变式由静态检查把关,而非交给 review:两张表都以 `satisfies` 对上述同一个 union 校验,因此只加其一而漏掉另一会在 `typecheck` 处失败。这正是该漂移风险应有的机械形式——运行期的 `Object.hasOwn` 守卫同样能捕获,但要等到有后端报告该语言之后:晚于漂移引入一个 PR,且触发点在消费方的集成处而非漂移引入处;在当前 base 上则永远不会触发,因为不存在第二个后端。两张表的声明类型仍是 `Record`,因为 `CodeRuntime.language` 是不受约束的 `string`:union 钉住本仓库交付了什么,守卫拒绝运行时报告了什么。落在这条检查之外的是点名已知值而非从中派生的散文:seam 侧的 `dsh-code-runtime` README 双语对、它的 `CodeRuntime.language` JSDoc 与 `docs/subsystems/code-runtime.md` 双语对,再加本包自己的 README 双语对与它的 `Config.mode` JSDoc。更早的 note 点名这些值时记的是其自身 PR 当时的状态,不在此列。让它无 gate 的是两条独立理由。其一,散文根本不受类型检查,union 放在哪里都一样。其二,类型级替代在这里也不可用:接口包不得 import 其消费方的表,而 `CodeRuntime.language` 按设计保持不受约束的 `string`,即便把 union 迁进 seam 也不会作用到它。用一个断言两张表键集相等的 unit test 的方案被否决:它买到的是同一条检查,代价却是把两张私有表做测试专用导出,且运行时机晚于编译器。对两张表都缺席的语言,两种运行期失败中报出哪一条随入口而异:组装路径报缺渲染器,因为 `wireSchemas` 在投影前先调 `requireCodeRuntime`;而公共 `schemas()` 先经过 `run_code` 的语言感知 getter,报的是缺 flavor 表项。工具层不依赖任何具体后端,因此它能先于 Python 协议和后端在 master 上落地并可测。 代价是两张表的 Python 分支在当前 base 上不可达:`CodeRuntime.language` 由所加载的后端设定,已发布的后端只有 `dsh-code-runtime-worker`(`'typescript'`),而注册表读取的是所加载的运行时而非某个配置字段,因此没有任何一份组装好的应用能选中 `renderToolsSdkPy` 或 `PYTHON_FLAVOR`。也就是说,在报告 `'python'` 的后端发布之前,本 note 的工作不改变模型可见表面,本 PR 的覆盖因此是 unit 级——渲染器输出加分发与拒绝路径。Python 模型界面的 keyless snapshot 归属于发布该后端的那个 PR,因为只有在那里,一份基于已发布插件的真实 `cordis.yml` 才会产出 Python 组装;在此处挂载 fixture 运行时的快照示例断言的是测试替身,而 [docs/testing.md](../../../../docs/testing.md) 明确拒绝以此替代组装好的应用 transcript。 diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml index cbe53b9622..d1cef43cc1 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.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-31-even-out-shipped-tool-rosters.md -2026-07-31-even-out-shipped-tool-rosters.md: d12db993654d9f2b41a16a4663dc64aefe8e3a2f -2026-07-31-even-out-shipped-tool-rosters.zh.md: 8381a457def1a909b300de52dc011e2437c7e9f3 +2026-07-31-even-out-shipped-tool-rosters.md: 0195620055da5e570d2f54792d950a88bab8d652 +2026-07-31-even-out-shipped-tool-rosters.zh.md: cc67f24720f650283daa4ee85a0a1af5358756a8 diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md index d12db99365..0195620055 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md @@ -22,7 +22,7 @@ Two rows stay surface-specific. `tmux-context` is TUI-only because a browser sur Three capabilities stay out on the evidence their own packages record, and are listed here so "we forgot" and "we decided against" stay distinguishable. -**`dsh-tool-cordis`** lets the model write JavaScript and mount it as a temporary plugin. Its README states the limit: "The sandbox is containment for honest code, not a security boundary — host-realm helpers on the sandbox global are reachable, so mount code can reach Node" ([Known limitations](../../../../packages/cordis/tool-cordis/README.md)). The `node:vm` realm lives inside the harness process while `dsh-sandbox-local` confines only the argv it spawns, so on the Web surface both the sandbox and the approval seam are bypassed rather than enforced. +**`dsh-tool-cordis`** lets the model write JavaScript and mount it as a temporary plugin. Its README states the limit: "The sandbox is containment for honest code, not a security boundary — host-realm helpers on the sandbox global are reachable, so mount code can reach Node" ([Known limitations](../../../../packages/self-modification/tool-cordis/README.md)). The `node:vm` realm lives inside the harness process while `dsh-sandbox-local` confines only the argv it spawns, so on the Web surface both the sandbox and the approval seam are bypassed rather than enforced. **`dsh-web-fetch-local`** stays unmounted and `dsh-tool-web` keeps `fetch: false`. SSRF protection is deferred in the implementation ([`policy.ts`](../../../../packages/web/web-fetch-local/src/policy.ts) validates protocol, credentials, and length only) and the package says so: "this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets" ([README](../../../../packages/web/web-fetch-local/README.md)). The model chooses the target, which includes the harness's own gateway on loopback, private ranges, and cloud metadata endpoints. diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md index 8381a457de..cc67f24720 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md @@ -22,7 +22,7 @@ Status: implemented 有三项能力基于其自身包所记录的证据保持在外,列在这里是为了让「我们忘了」和「我们决定不要」保持可区分。 -**`dsh-tool-cordis`** 让模型写一段 JavaScript 并挂成临时插件。它的 README 写明了这个界限:「The sandbox is containment for honest code, not a security boundary — host-realm helpers on the sandbox global are reachable, so mount code can reach Node」([Known limitations](../../../../packages/cordis/tool-cordis/README.md))。`node:vm` 的 realm 就在 harness 进程内,而 `dsh-sandbox-local` 只约束它 spawn 出去的 argv,因此在 Web surface 上,沙箱与批准接缝是被绕过而非被执行。 +**`dsh-tool-cordis`** 让模型写一段 JavaScript 并挂成临时插件。它的 README 写明了这个界限:「The sandbox is containment for honest code, not a security boundary — host-realm helpers on the sandbox global are reachable, so mount code can reach Node」([Known limitations](../../../../packages/self-modification/tool-cordis/README.md))。`node:vm` 的 realm 就在 harness 进程内,而 `dsh-sandbox-local` 只约束它 spawn 出去的 argv,因此在 Web surface 上,沙箱与批准接缝是被绕过而非被执行。 **`dsh-web-fetch-local`** 保持不挂,`dsh-tool-web` 保持 `fetch: false`。SSRF 防护在实现中是 deferred 状态([`policy.ts`](../../../../packages/web/web-fetch-local/src/policy.ts) 只校验协议、凭据与长度),包里也直说了:「this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets」([README](../../../../packages/web/web-fetch-local/README.md))。目标由模型选择,其中包括 harness 自己跑在环回地址上的网关、内网段和云元数据端点。 diff --git a/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml index 4acabc8700..def34cdfbd 100644 --- a/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml +++ b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md -2026-06-20-core-data-structures-catalog.md: 7ee1e0ac3df7cb37fc9797702d44f409da820a94 -2026-06-20-core-data-structures-catalog.zh.md: 7cb0ae216f5c5f429c18d097862350997a8335d3 +2026-06-20-core-data-structures-catalog.md: 9a3f80df592f28adc365d5e64cc15516d92b47b5 +2026-06-20-core-data-structures-catalog.zh.md: 96c606a3d65a67e757960c5cbc997e717f11e8d5 diff --git a/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md index 7ee1e0ac3d..9a3f80df59 100644 --- a/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md +++ b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md @@ -1,4 +1,4 @@ -# Agent Note: Core-data-structures catalog and the `ts type-equiv` drift gate +# Agent Note: Subsystems catalog and the `ts type-equiv` drift gate Status: implemented @@ -12,20 +12,22 @@ So the work had two intertwined questions: **what belongs in such a catalog** (t ## Decision -A new `docs/core-data-structures/` folder catalogs the vocabulary, with a new `verify-type-equiv` doc-sync gate that keeps every pasted type declaration and its JSDoc synchronized with source. +A new `docs/subsystems/` folder catalogs the vocabulary, with a new `verify-type-equiv` doc-sync gate that keeps every pasted type declaration and its JSDoc synchronized with source. ### What counts as "core" — the spine-vs-seam line -The scoping line was not picked top-down; it was discovered by testing candidate definitions against concrete borderline types until one rule survived every case. The decisive test was `BashExecRequest`/`BashExecSpec`/`BashRunResult`: bash is a capability *seam*, not part of the agent-loop spine, so if those are "core" then "core" means *all cross-package vocabulary* and the catalog is a flat dump; if they are not, "core" means *the central spine* and bash vocabulary belongs on a sub-page. The latter won, which set the whole structure: a **tiered folder**, not a flat document. +> **Superseded as the page-scoping rule** by [package-anchored subsystem pages](2026-08-03-package-anchored-subsystem-pages.md): each page now anchors to the package group that declares its vocabulary. The `ts type-equiv` mechanism below remains current. -The rule that settled the remaining cases: ***the type you write, hold, or receive is core; the machinery that types it, renders it, or persists it is a sub-page detail.*** Worked through: +The scoping line was not picked top-down; it was discovered by testing candidate definitions against concrete borderline types until one rule survived every case. The decisive test was `BashExecRequest`/`BashExecSpec`/`BashRunResult`: bash is a capability *seam*, not part of the agent-loop spine, so if those are "core" then "core" means *all cross-package vocabulary* and the catalog is a flat dump; if they are not, "core" means *the central spine* and bash vocabulary belongs on its own seam page. The latter won, which set the whole structure: a **tiered folder**, not a flat document. + +The rule that settled the remaining cases: ***the type you write, hold, or receive is core; the machinery that types it, renders it, or persists it is a seam-page detail.*** Worked through: - A data structure is **core** if it flows through the agent-loop spine — the loop holds, derives, streams, or logs it on every turn regardless of which plugins load (`Message`, `StreamChunk`, `SessionEvent`, the `Agent` handle) — **or** it is the single headline type a plugin author writes against a pipeline (`ToolDefinition`). -- `ToolDefinition` is core (it is what every tool author writes) **even though the loop never holds one** — authoring-importance overrides the strict flows-through-spine rule for this one headline type. But its typing machinery — `ValueSchemaSpec`, `ParameterSchemaSpec`, `InferValue`, and `InferArgs` — is a sub-page detail. That is the spine-vs-seam line made sharp. +- `ToolDefinition` is core (it is what every tool author writes) **even though the loop never holds one** — authoring-importance overrides the strict flows-through-spine rule for this one headline type. But its typing machinery — `ValueSchemaSpec`, `ParameterSchemaSpec`, `InferValue`, and `InferArgs` — is a seam-page detail. That is the spine-vs-seam line made sharp. - `ToolSchema` is core (it is a field of `GenerateOptions`, the model request that flows through every step) even though it is conceptually part of the tool pipeline — *flows through the spine* wins over *conceptual home* when they conflict. -- The tool-presentation vocabulary (`ToolCallView`/`ToolResultView`, …), the `SessionPersistence` durability seam, and bash vocabulary are sub-pages. +- The tool-presentation vocabulary (`ToolCallView`/`ToolResultView`, …), the `SessionPersistence` durability seam, and bash vocabulary are seam pages. -`core.md` is a **self-contained spine doc**: it states the exact type definition of each spine structure with minimal prose and links to sub-pages for the per-seam detail. The sub-pages are `llm-streaming.md`, `session.md`, `persistence.md` (split from session along the in-memory-model vs. durability-seam line), `tools.md`, and `bash.md`. +`core.md` is a **self-contained spine doc**: it states the exact type definition of each spine structure with minimal prose and links to sibling seam pages for the per-seam detail; the folder's [README](../../../../docs/subsystems/README.md) indexes every page. The original seam pages are `llm-streaming.md`, `session.md`, `persistence.md` (split from session along the in-memory-model vs. durability-seam line), `tools.md`, and `bash.md`. ### The `ts type-equiv` mechanism — literal AND drift-proof @@ -58,3 +60,4 @@ The spine-vs-seam rule was tested against `BashExecRequest`, tool schemas and de - The spine-vs-seam line is a reusable scoping tool, not a one-off: the same "the thing you write/hold/receive is core; the machinery that types/renders/persists it is a detail" rule is what later scoped the events/services catalog's harness-vs-inherited tiering. - The `ts type-equiv` fence is a third doc-block category alongside ` ```ts ` (compiled) and ` ```ts ignore-check ` (sketch). A later sibling added a fourth, ` ```ts cordis-catalog ` (generated signature), reusing the same skip-and-exclude treatment. - Adding or reshaping a core type now carries a documentation obligation the author must honor (the gate cannot detect a missing *new* type), backstopped by the `dsh-code-review` checklist. +- Since 2026-07-27 the seam-page tier spans every service-bearing subsystem: nine lean pages (permission presets, plan mode, runtime invariants, the HTTP carrier, storage — owning both `ctx.storage` and `ctx.storageDomain` — TUI extensions, workspaces, client modules, telemetry) cover the ten `ctx` services that had none, so each harness service and event scope has exactly one owning subsystems page — the precondition for generating per-subsystem service/event reference into these pages instead of flat catalogs. diff --git a/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.zh.md b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.zh.md index 7cb0ae216f..96c606a3d6 100644 --- a/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.zh.md +++ b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.zh.md @@ -12,10 +12,12 @@ Status: implemented ## 决策 -新增的 `docs/core-data-structures/` 目录对这些词汇编目,并配有新的 `verify-type-equiv` doc-sync(文档同步门禁),使每个粘贴的类型声明及其 JSDoc 与源码保持同步。 +新增的 `docs/subsystems/` 目录对这些词汇编目,并配有新的 `verify-type-equiv` doc-sync(文档同步门禁),使每个粘贴的类型声明及其 JSDoc 与源码保持同步。 ### 何为「核心」——主干与 seam 的分界线 +> **作为页面范围界定规则已被取代**,见[按包锚定的子系统页面](2026-08-03-package-anchored-subsystem-pages.md):每页现在锚定到声明其词汇的包分组。下文的 `ts type-equiv` 机制仍然有效。 + 范围界定并非自上而下拍定,而是将候选定义逐一对照具体的边界类型反复测试,直到一条规则在所有案例中都成立。决定性的测试是 `BashExecRequest`/`BashExecSpec`/`BashRunResult`:bash 是一个能力 *seam*,不属于 agent loop(智能体循环)主干;如果这些算「核心」,那么「核心」就意味着*所有跨包词汇*,目录沦为平铺罗列;如果不算,「核心」就意味着*中央主干*,bash 词汇归入子页面。后者胜出,由此确定了整体结构:一个**分层文件夹**,而非一份平铺文档。 确定其余案例的规则是:***你编写、持有或接收的类型是核心;为其提供类型推导、渲染或持久化的机制是子页面细节。*** 逐一验证如下: @@ -25,7 +27,7 @@ Status: implemented - `ToolSchema` 是核心(它是流经每个步骤的模型请求 `GenerateOptions` 的一个字段),即使它在概念上属于工具流水线——当*流经主干*与*概念归属*冲突时,前者胜出。 - 工具展示词汇(`ToolCallView`/`ToolResultView` 等)、`SessionPersistence` 持久性 seam 以及 bash 词汇是子页面。 -`core.md` 是一份**自包含的主干文档**:它给出每个主干结构的确切类型定义,辅以最少的行文,并链接到子页面获取各 seam 的细节。子页面包括 `llm-streaming.md`、`session.md`、`persistence.md`(沿内存模型与持久性 seam 的分界线从会话页面拆出)、`tools.md` 和 `bash.md`。 +`core.md` 是一份**自包含的主干文档**:它给出每个主干结构的确切类型定义,辅以最少的行文,并链接到同级 seam 页面获取各 seam 的细节;目录的 [README](../../../../docs/subsystems/README.md) 索引全部页面。最初的 seam 页面包括 `llm-streaming.md`、`session.md`、`persistence.md`(沿内存模型与持久性 seam 的分界线从会话页面拆出)、`tools.md` 和 `bash.md`。 ### `ts type-equiv` 机制——既逐字又防漂移 @@ -58,3 +60,4 @@ Status: implemented - 主干与 seam 分界线是一个可复用的范围界定工具,而非一次性的:同一条「你编写/持有/接收的东西是核心;为其提供类型推导/渲染/持久化的机制是细节」规则,后来也被用于界定事件/服务目录的 harness 层与继承层分层。 - `ts type-equiv` 围栏是继 ` ```ts `(编译)和 ` ```ts ignore-check `(草稿)之后的第三种文档块类别。后续的姊妹门禁又增加了第四种 ` ```ts cordis-catalog `(生成签名),复用了相同的跳过并排除处理。 - 添加或重塑核心类型现在附带一项文档义务,作者必须履行(门禁无法检测缺失的*新*类型),由 `dsh-code-review` 检查清单兜底。 +- 自 2026-07-27 起,子页面层级覆盖每个承载服务的子系统:九个精简页面(权限预设、计划模式、运行时不变式、HTTP 载体、存储——同时拥有 `ctx.storage` 与 `ctx.storageDomain`——终端扩展、工作区、客户端模块、遥测)覆盖了原先没有页面的十个 `ctx` 服务,于是每个 harness 服务和事件作用域都有恰好一个所属的 subsystems 页面——这是把按子系统生成的服务/事件参考写入这些页面(而非平铺目录)的前提。 diff --git a/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.i18n.yaml b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.i18n.yaml index b1098546b7..d97c4dd0b7 100644 --- a/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md -2026-07-02-tool-schema-catalog.md: c8cc69df428f6eee0f66ed976865afe2a0702448 -2026-07-02-tool-schema-catalog.zh.md: a1af928cc799974b93d2c8e29792790bc1f4e9e6 +2026-07-02-tool-schema-catalog.md: 673b51831512662c4e2cc3b79cd85b149c78f585 +2026-07-02-tool-schema-catalog.zh.md: 8e47d807a2490e362bb3db6d1d634c75c60997e3 diff --git a/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md index c8cc69df42..673b518315 100644 --- a/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md +++ b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md @@ -6,7 +6,7 @@ English | [中文](2026-07-02-tool-schema-catalog.zh.md) ## Problem -The repository had no single reference for the names, descriptions, and JSON Schemas actually exposed to the model. Source declarations are scattered and runtime-composed, while the existing Cordis and data-structure catalogs cover wiring and vocabulary rather than tools. +The repository had no single reference for the names, descriptions, and JSON Schemas actually exposed to the model. Source declarations are scattered and runtime-composed, while the existing Cordis reference and subsystem pages cover wiring and vocabulary rather than tools. ## Decision diff --git a/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.zh.md b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.zh.md index a1af928cc7..8e47d807a2 100644 --- a/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.zh.md +++ b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.zh.md @@ -6,7 +6,7 @@ Status: implemented ## 问题 -仓库此前没有一份统一的参考文档来记录实际暴露给模型的工具名称、描述与 JSON Schema。源码声明分散各处且在运行时组合,而既有的 Cordis 目录和数据结构目录覆盖的是接线与词汇,而非工具。 +仓库此前没有一份统一的参考文档来记录实际暴露给模型的工具名称、描述与 JSON Schema。源码声明分散各处且在运行时组合,而既有的 Cordis 参考和子系统页面覆盖的是接线与词汇,而非工具。 ## 决策 diff --git a/.agents/notes/implemented/process/2026-07-06-node-engine-floor.i18n.yaml b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.i18n.yaml index d96a3b4d6c..ddca590dea 100644 --- a/.agents/notes/implemented/process/2026-07-06-node-engine-floor.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-06-node-engine-floor.md -2026-07-06-node-engine-floor.md: e42b809f83fe388526ecbd74192989e0bcdbba67 -2026-07-06-node-engine-floor.zh.md: 92fa00f7a270f4506cba6c899e9edf6a2fdd3209 +2026-07-06-node-engine-floor.md: ad21fbfe034cbf4bb7b75aaac4113f3139a1832d +2026-07-06-node-engine-floor.zh.md: 0d1c31ed74565f9b20116cd0e666246baad9bf79 diff --git a/.agents/notes/implemented/process/2026-07-06-node-engine-floor.md b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.md index e42b809f83..ad21fbfe03 100644 --- a/.agents/notes/implemented/process/2026-07-06-node-engine-floor.md +++ b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.md @@ -14,7 +14,7 @@ Set `engines.node` to `^22.19.0 || >=24.0.0` and test keyless CI on `['22.19', 2 Two Node features gate the source runtime: -- **`node:sqlite`** — `packages/session-persistence/session-persistence-sqlite` does a top-level `import { DatabaseSync } from 'node:sqlite'`. The module dropped its `--experimental-sqlite` flag requirement at **22.13** (LTS) and **23.4** (Current); before those, importing it throws at load. +- **`node:sqlite`** — `packages/session/session-persistence-sqlite` does a top-level `import { DatabaseSync } from 'node:sqlite'`. The module dropped its `--experimental-sqlite` flag requirement at **22.13** (LTS) and **23.4** (Current); before those, importing it throws at load. - **Native TypeScript type-stripping** — the built-mode `examples/headless-agent/tests/keyless-smoke.e2e.ts` smoke boots its unexported `.ts` driver under plain `node` (no tsx) and loads the example's `.ts` test adapter (`cli-mock-llm.ts`). Type-stripping is the default from **22.18** (LTS) and **23.6** (Current); before those it needs `--experimental-strip-types`. Those source features clear on the 22.x line at **22.18**, but the installed Pi adapter dependency raises the advertised LTS floor. `@deepseek-ai/dsh-llm-pi-ai` depends on `@earendil-works/pi-ai@0.79.3`, whose package declares `engines.node >=22.19.0`, so the LTS floor is **22.19**. The 24.x branch remains `>=24.0.0`. The disjoint range excludes Node 23 entirely: Node 23.0–23.5 still has at least one flagged source feature, and the 23 line is non-LTS/EOL, so advertising `>=23.6` would add a dead release line and a CI leg no deployment should use. diff --git a/.agents/notes/implemented/process/2026-07-06-node-engine-floor.zh.md b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.zh.md index 92fa00f7a2..0d1c31ed74 100644 --- a/.agents/notes/implemented/process/2026-07-06-node-engine-floor.zh.md +++ b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.zh.md @@ -14,7 +14,7 @@ Status: implemented 两个 Node 特性决定了源码运行时的门槛: -- **`node:sqlite`**:`packages/session-persistence/session-persistence-sqlite` 在顶层执行 `import { DatabaseSync } from 'node:sqlite'`。该模块在 **22.13**(LTS)和 **23.4**(Current)取消了 `--experimental-sqlite` 标志要求;在此之前,导入它会在加载时抛出异常。 +- **`node:sqlite`**:`packages/session/session-persistence-sqlite` 在顶层执行 `import { DatabaseSync } from 'node:sqlite'`。该模块在 **22.13**(LTS)和 **23.4**(Current)取消了 `--experimental-sqlite` 标志要求;在此之前,导入它会在加载时抛出异常。 - **原生 TypeScript 类型剥离**——构建模式的 `examples/headless-agent/tests/keyless-smoke.e2e.ts` 冒烟测试使用纯 `node`(无 tsx)启动该示例未导出的 `.ts` driver,并加载示例的 `.ts` 测试适配器(`cli-mock-llm.ts`)。类型剥离从 **22.18**(LTS)和 **23.6**(Current)起成为默认行为;更早版本需要 `--experimental-strip-types`。 这些源码特性在 22.x 线上于 **22.18** 全部就绪,但已安装的 Pi 适配器依赖将宣传的 LTS 下限进一步提高。`@deepseek-ai/dsh-llm-pi-ai` 依赖 `@earendil-works/pi-ai@0.79.3`,后者的包声明 `engines.node >=22.19.0`,因此 LTS 下限为 **22.19**。24.x 分支保持 `>=24.0.0`。该不相交范围完全排除了 Node 23:Node 23.0–23.5 至少还有一个源码特性需要标志,而 23 线是非 LTS/已 EOL 的,宣传 `>=23.6` 会增加一条已终止的发布线和一条 CI 分支,而没有任何部署应当使用它。 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/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml new file mode 100644 index 0000000000..7bc08eb82d --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md +2026-08-03-package-anchored-subsystem-pages.md: 5ba7937f0c142727d343a107a3ff87f5f337931e +2026-08-03-package-anchored-subsystem-pages.zh.md: 27d56158a69a8144b0915054032fb3b54493bd0b diff --git a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md new file mode 100644 index 0000000000..5ba7937f0c --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md @@ -0,0 +1,34 @@ +# Agent Note: Package-anchored subsystem pages and thin group READMEs + +Status: implemented + +English | [中文](2026-08-03-package-anchored-subsystem-pages.zh.md) + +## Problem + +The [subsystems catalog](2026-06-20-core-data-structures-catalog.md) scoped its front page by the spine-vs-seam rule: a type was "core" if the loop holds, derives, streams, or logs it on every turn. That rule selected types, not packages, so as the folder grew to forty-plus pages the front page became a cross-package grab-bag: LLM conversation vocabulary sat above the agent contracts, the creation/ownership vocabulary (`AgentHandle`, `CreateAgentOptions`, `ResumeAgentOptions`, `AgentFactory`) was documented nowhere in the folder because the generator exempted it to a package README, and a reader could not predict which page documents a type from where the type lives. Package-group READMEs meanwhile had no common shape — some carried sectioned tables, stray design essays, or trailing paragraphs that belonged on a subsystem page. + +## Decision + +Every `docs/subsystems/` page anchors to the package or package group that declares its vocabulary, and page membership follows the repository layout: [core.md](../../../../docs/subsystems/core.md) is the `packages/core` page (creation and ownership, the `Agent` handle with its delivery/cancellation/interception contracts, pointers to the group's dedicated pages), [llm-streaming.md](../../../../docs/subsystems/llm-streaming.md) owns `packages/llm` end-to-end, and so on. Repo-wide type patterns (`…Map → derived-union`, branded ids) stay on core.md in an explicitly framed closing section rather than interleaved with the package content. This supersedes the spine-vs-seam rule *as the page-scoping rule*; the placement heuristic that survives is simpler: a type is documented where its declaring package's page is, and machinery keeps living with its machinery. + +Every type a generated signature references must resolve somewhere in the folder: the agent ownership vocabulary moved from the generator's `TYPE_LINK_EXEMPTIONS` into `LINK_MAP → core.md`, so exemptions are reserved for genuinely service-local or vendored shapes. Each pasted declaration has one home (`SessionEvent` lives on [session.md](../../../../docs/subsystems/session.md); core.md summarizes and links). + +Every `packages//README.md` pair is a thin front door in one shape: a why-first intro paragraph, a package table (Package / Role / ctx key), and a closing pointer to the owning subsystems page. Load-bearing prose that outgrows that shape relocates to the owning subsystems page rather than being deleted. + +The [subsystems README](../../../../docs/subsystems/README.md) indexes every page in the folder on both language sides; `scripts/project-doc-site.spec.ts` enforces one table row per page, so a page added by a later PR (or absorbed in a merge) cannot silently miss the index. + +## Alternatives considered + +**Keep the spine-vs-seam scoping rule.** It answered "is this type core?" per type, which is why the front page accumulated types from four packages while missing half of `packages/core/agent`'s public surface. Predictability by repository layout won. + +**A flat single-document catalog.** Already rejected in the [original catalog note](2026-06-20-core-data-structures-catalog.md); the growth to forty-one pages confirmed that verdict. + +**Document ownership vocabulary only in package READMEs (the exemption status quo).** This left `AgentHandle` and the create/resume options invisible to the folder that claims to be the type reference, and the generated `Types:` footers could not link them. + +## Consequences + +- Which page documents a type is predictable from `packages//`; the subsystems README is a complete index enforced by test. +- Generated signature footers link the agent ownership vocabulary instead of silently exempting it. +- `verify-type-equiv`'s 1:1 manifest keeps each paste single-homed; the duplicate `SessionEvent` paste is gone. +- The [original catalog note](2026-06-20-core-data-structures-catalog.md) remains the owner of the `ts type-equiv` drift-gate mechanism; only its page-scoping rule is superseded here. diff --git a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md new file mode 100644 index 0000000000..27d56158a6 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md @@ -0,0 +1,34 @@ +# Agent Note:按包锚定的子系统页面与轻薄的分组 README + +Status: implemented + +[English](2026-08-03-package-anchored-subsystem-pages.md) | 中文 + +## Problem + +[子系统目录](2026-06-20-core-data-structures-catalog.md)最初用主干-vs-seam 规则界定首页范围:如果循环在每个轮次都持有、派生、流式传输或记录某个类型,它就是「核心」。该规则选择的是类型而非包,因此当目录增长到四十多页后,首页变成了跨包大杂烩:LLM 对话词汇排在 agent 契约之前,创建/所有权词汇(`AgentHandle`、`CreateAgentOptions`、`ResumeAgentOptions`、`AgentFactory`)在目录中无处记录(生成器把它们豁免给了某个包 README),读者无法根据类型所在位置预测哪一页记录它。与此同时,各包分组 README 没有统一形状——有的带分节表格、游离的设计短文,或本应属于子系统页面的尾部段落。 + +## Decision + +每个 `docs/subsystems/` 页面锚定到声明其词汇的包或包分组,页面归属跟随仓库布局:[core.md](../../../../docs/subsystems/core.md) 是 `packages/core` 的页面(创建与所有权、`Agent` 句柄及其投递/取消/拦截契约、指向该组专属页面的指针),[llm-streaming.md](../../../../docs/subsystems/llm-streaming.md) 端到端拥有 `packages/llm`,依此类推。全仓通用类型模式(`…Map → 派生联合`、品牌化 id)保留在 core.md 一个明确标注的收尾小节中,而不是与包内容交错。这在*页面范围界定规则*的意义上取代了主干-vs-seam 规则;存活下来的放置启发式更简单:类型记录在其声明包对应的页面,机制仍与其机制放在一起。 + +生成签名引用的每个类型都必须能在目录中某处解析:agent 所有权词汇从生成器的 `TYPE_LINK_EXEMPTIONS` 移入 `LINK_MAP → core.md`,因此豁免只留给真正服务本地或 vendored 的形状。每个粘贴的声明只有一个家(`SessionEvent` 位于 [session.md](../../../../docs/subsystems/session.md);core.md 概括并链接)。 + +每个 `packages//README.md` 配对都是统一形状的轻薄门面:一段以「为什么」开头的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向拥有方子系统页面的收尾指针。超出该形状的承重散文迁移到拥有方子系统页面,而非删除。 + +[子系统 README](../../../../docs/subsystems/README.md) 在两个语言侧索引目录中的每一页;`scripts/project-doc-site.spec.ts` 强制每页一行表格,因此后续 PR 新增(或合并吸收)的页面无法悄悄缺席索引。 + +## Alternatives considered + +**保留主干-vs-seam 界定规则。**它逐类型回答「这个类型是核心吗?」,这正是首页积累了四个包的类型、却缺失 `packages/core/agent` 一半公开 surface 的原因。按仓库布局可预测性胜出。 + +**扁平的单文档目录。**在[原目录 note](2026-06-20-core-data-structures-catalog.md) 中已被否决;增长到四十一页证实了该结论。 + +**只在包 README 中记录所有权词汇(豁免的现状)。**这让 `AgentHandle` 与 create/resume 选项在自称类型参考的目录中不可见,生成的 `Types:` 页脚也无法链接它们。 + +## Consequences + +- 哪一页记录某类型可由 `packages//` 预测;子系统 README 是由测试强制的完整索引。 +- 生成的签名页脚链接 agent 所有权词汇,而不是静默豁免。 +- `verify-type-equiv` 的 1:1 manifest 保证每个粘贴单一归属;重复的 `SessionEvent` 粘贴已移除。 +- [原目录 note](2026-06-20-core-data-structures-catalog.md) 仍拥有 `ts type-equiv` 漂移检查机制;此处仅取代其页面范围界定规则。 diff --git a/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.i18n.yaml b/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.i18n.yaml new file mode 100644 index 0000000000..0c44f790b7 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md +2026-08-08-automatic-translation-pairing-merges.md: ac4e0c11290abfd48d0f3caf037062aa2fe36c4d +2026-08-08-automatic-translation-pairing-merges.zh.md: 38e8c0a5613a6bd0b3e76143a93c93653430d0c2 diff --git a/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md b/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md new file mode 100644 index 0000000000..ac4e0c1129 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md @@ -0,0 +1,55 @@ +# Agent Note: Automatically compose translation pairing records + +Status: implemented + +English | [中文](2026-08-08-automatic-translation-pairing-merges.zh.md) + +## Problem + +A bilingual consistency record contains the two owner files' exact blob hashes. Two branches that independently update different parts of the same confirmed pair therefore conflict on both hash lines even when Git cleanly composes both Markdown owners. Selecting either side leaves stale hashes, while regenerating the record by hand repeats a deterministic operation and prevents an otherwise automatic merge. + +## Decision + +`*.i18n.yaml` uses the repository-owned `dsh-translation-pairing` merge driver. The worktree-local Git installer registers its command alongside Lefthook setup; Git configuration remains local because a tracked attribute can name a driver but cannot carry its executable command. + +The installer loads the exact Node/tsx entrypoint before publishing worktree integration. Git invokes a checked-in shell launcher that does not require Node and repeats this probe before every driver execution. When the runtime or entrypoint is unavailable, the launcher materializes Git's ordinary three-way text result in the sidecar but returns a conflict even when that text merge is clean, so Git retains the unmerged index stages and never accepts unverified metadata. + +The driver parses the ancestor, current, and other records and loads the six owner blobs named by their hashes. It independently runs Git's default three-way text merge for the English and Chinese triplets, requires both merges to be clean, verifies language switchers and the pairing structural signature, stores the two merged blobs, and writes their hashes as the canonical record. This composes confirmations already present in both parents; it never records an ordinary one-sided documentation edit. + +The driver fails with an ordinary unresolved sidecar when a record is malformed, an object is missing, an owner uses another merge strategy (including a non-text `merge.default` inherited by an otherwise unspecified path), either owner has content conflicts, or the merged pair violates structural checks. Add/delete and rename shapes remain manual because their path ownership is not the same three-record operation. + +`pnpm run resolve-translation-pairing-conflicts` applies the same algorithm after a merge has already stopped. Before writing any sidecar, it proves that the sidecar still contains Git's untouched conflict result and that the staged owner blob IDs and working-tree bytes equal its independent merges. It writes and stages every safe record as one batch even when another pair still needs manual work, then reports the remaining pairing conflicts and exits unsuccessfully so callers cannot mistake a partial resolution for a completed merge. + +`pre-merge-commit` and `pre-commit` verify staged `.i18n.yaml` files against the exact index bytes of their owners. They validate driver output but do not regenerate records, so bypassing a hook cannot silently bless translation drift; the corpus-wide `doc-sync` check remains authoritative in CI. + +## Failure contract + +| Failure during a normal `git merge` | Observable state | Recovery | +|---|---|---| +| A fresh install cannot probe the driver or install Lefthook | No new driver or hook-path configuration is published; any newly added integration is rolled back to the previous hook lookup. | Restore the dependencies and rerun `node scripts/install-lefthook.mjs`. | +| Node, tsx, or the driver entrypoint becomes unavailable after installation | The merge stops with the sidecar at `UU`, index stages 1/2/3 remain, the worktree sidecar contains Git's text result, `MERGE_HEAD` exists, and no commit is created. | Restore the dependencies and run `pnpm run resolve-translation-pairing-conflicts`, or run `git merge --abort`. | +| The repository-aware driver rejects the records | The merge stops with the sidecar unresolved and no commit; the driver prints the owner-repair and explicit-resolver path. | Repair the owner conflict or record, then run the printed resolver workflow or abort. | +| The driver process crashes with a status above 128 | Git aborts the merge strategy without publishing `MERGE_HEAD` or unmerged index stages. | Repair the runtime and rerun the merge. | +| `pre-merge-commit` rejects an otherwise clean file merge | No unmerged entries remain, the complete result is staged with `MERGE_HEAD`, and no merge commit is created. | Repair the hook failure and run `git commit`, or run `git merge --abort`. | + +An installer rollback failure reports both the original installation error and every rollback error. Because worktree configuration may then be partial, the contributor repairs or inspects it before merging instead of relying on a silent fallback. + +## Verification + +Script tests exercise clean composition through the installed launcher, missing-runtime and broken-entrypoint text fallback, installer probe rollback, a rejecting `pre-merge-commit` hook, explicit recovery from an unresolved index, mixed safe and owner-conflicted pairs, edited sidecars, non-text default merge configuration, record parsing, and worktree-local installation. The existing corpus verifier continues to prove that a committed record matches its two owners. + +## Alternatives considered + +**Take ours or use Git's union driver.** Either parent record names pre-merge content, while union produces duplicate or unordered hash keys. Neither represents the merged owners. + +**Regenerate in `post-merge` or only in a commit hook.** `post-merge` does not run after a conflicted merge and cannot affect its outcome. Commit hooks are reached only after the index has no unresolved entries, so a hook alone cannot clear the generated conflict. + +**Wrap every merge in a repository command.** A wrapper can resolve the conflict from the populated index, and the explicit resolver retains that recovery path, but raw Git, stack tooling, rebases, and cherry-picks would still stop before it. The merge driver is the file-level extension point shared by those operations. + +**Resolve on GitHub through Actions or an app.** Hosted automation could update PR branches, but it adds credentials, concurrency control, and branch mutation. Local and agent-driven merge-forward workflows already have a checkout and push authority; the repository keeps remote automation out of this mechanism. + +## Consequences + +Installed worktrees automatically remove pairing-record-only conflicts while preserving human judgment for owner conflicts and translation quality. GitHub's hosted mergeability calculation does not run the worktree-local executable, so a contributor or agent must still merge the base and push the resulting commit before the remote conflict badge clears. + +The installer reserves `merge.dsh-translation-pairing.*` in worktree configuration and refuses a conflicting custom value. Automatic composition depends on the installed Node dependencies, like the repository's contributor hooks; runtime loss produces a visible unresolved text result rather than selecting stale metadata. diff --git a/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.zh.md b/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.zh.md new file mode 100644 index 0000000000..38e8c0a561 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.zh.md @@ -0,0 +1,55 @@ +# Agent Note: 自动组合翻译配对记录 + +Status: implemented + +[English](2026-08-08-automatic-translation-pairing-merges.md) | 中文 + +## 问题 + +一份双语一致性记录包含两侧 Markdown 文件的精确 blob hash。因此,当两个分支分别更新同一已确认配对的不同部分时,即使 Git 能干净合并两侧 Markdown 文件,记录中的两行 hash 仍会发生冲突。选择任一侧都会留下陈旧 hash;手工重新生成记录则会重复执行一项确定性操作,并阻止本可自动完成的合并。 + +## 决策 + +`*.i18n.yaml` 使用由仓库维护的 `dsh-translation-pairing` 合并驱动。worktree 本地的 Git 安装脚本在设置 Lefthook 的同时注册该驱动命令;Git 配置仍限定于当前 worktree,因为受跟踪的属性可以指定驱动,却无法携带其可执行命令。 + +安装脚本会在发布 worktree 集成前加载确切的 Node/tsx 入口点。Git 会调用仓库内已提交且不依赖 Node 的 shell 启动器;该启动器会在每次执行驱动前重复这项探测。运行时或入口点不可用时,即使文本合并干净完成,启动器也会将 Git 的普通三方文本合并结果写入伴随文件并返回冲突状态,使 Git 保留未合并的索引阶段,绝不接受未经验证的元数据。 + +驱动会解析祖先、当前和另一侧的记录,并加载其中 hash 指向的六个 Markdown blob。它分别对英文文档的三个 blob 与中文文档的三个 blob 运行 Git 默认的三方文本合并,要求两次合并都干净完成,校验语言切换行与配对结构签名,存储两个合并后的 blob,再将其 hash 写入规范记录。这一过程组合两个父提交中已经存在的确认;它绝不会为普通的单侧文档编辑写入记录。 + +记录格式错误、对象缺失、配对文档采用其他合并策略(包括原本未单独指定策略的路径继承了非文本 `merge.default`)、任一配对文档存在内容冲突,或合并后的配对违反结构检查时,驱动会让伴随文件保持为普通的未解决冲突。新增、删除及重命名情形仍需手工处理,因为它们的路径归属关系无法由上述基于三份记录的操作处理。 + +`pnpm run resolve-translation-pairing-conflicts` 会在合并已经停止后应用同一算法。写入任何伴随文件之前,它会验证伴随文件仍保留 Git 生成且未经改动的冲突结果,并验证暂存区中配对文档的 blob ID 以及工作树中的文件字节都与工具独立完成的合并结果一致。即使其他配对仍需手工处理,它也会将所有可安全生成的记录作为一个批次写入并暂存;随后报告剩余的配对冲突并以非零状态退出,确保调用方不会把仅部分解决误认为合并已经完成。 + +`pre-merge-commit` 与 `pre-commit` 通过限定范围的配对检查,对照两侧文档在索引中的精确字节校验暂存的 `.i18n.yaml` 文件。这些钩子会验证驱动输出,但不会重新生成记录,因此绕过钩子也无法默默确认发生漂移的译文;CI 中全语料的 `doc-sync`(文档同步门禁)仍是权威检查。 + +## 失败契约 + +| 普通 `git merge` 期间的失败 | 可观察状态 | 恢复 | +|---|---|---| +| 全新安装无法探测驱动或安装 Lefthook | 不会发布新的驱动或钩子路径配置;任何新增集成都会回滚到原有的钩子查找方式。 | 恢复依赖并重新运行 `node scripts/install-lefthook.mjs`。 | +| 安装后 Node、tsx 或驱动入口点变得不可用 | 合并停止,伴随文件状态为 `UU`;索引阶段 1/2/3 保留,worktree 中的伴随文件包含 Git 的文本结果,`MERGE_HEAD` 存在,且不创建提交。 | 恢复依赖并运行 `pnpm run resolve-translation-pairing-conflicts`,或运行 `git merge --abort`。 | +| 了解仓库状态的驱动拒绝记录 | 合并停止,伴随文件保持未解决状态且不创建提交;驱动会打印修复配对文档和运行显式冲突解决命令的路径。 | 修复配对文档冲突或记录,然后运行打印出的冲突解决工作流,或中止合并。 | +| 驱动进程以大于 128 的状态码崩溃 | Git 中止合并策略,既不写入 `MERGE_HEAD`,也不产生未合并的索引阶段。 | 修复运行时并重新执行合并。 | +| `pre-merge-commit` 拒绝原本能干净完成的文件合并 | 不再有未合并条目,完整结果已暂存且 `MERGE_HEAD` 存在,但不会创建合并提交。 | 修复钩子失败并运行 `git commit`,或运行 `git merge --abort`。 | + +安装脚本回滚失败时,会同时报告最初的安装错误和每一项回滚错误。此时 worktree 配置可能只完成了一部分,因此贡献者会先修复或检查配置再执行合并,而不是依赖静默回退。 + +## 验证 + +脚本测试通过已安装的启动器覆盖无冲突组合,并覆盖运行时缺失与入口点损坏时的文本回退、安装脚本探测回滚、拒绝合并的 `pre-merge-commit` 钩子、从含未解决条目的索引中显式恢复、可安全解决的配对与配对文档冲突的配对并存、被编辑过的伴随文件、非文本默认合并配置、记录解析和 worktree 本地安装。既有的全语料校验器继续证明已提交的记录与两侧配对文档相符。 + +## 考虑过的替代方案 + +**选择 ours 或使用 Git 的 union 驱动。** 任一父提交的记录都指向合并前的内容,而 union 会产生重复或乱序的 hash 键。两者都无法表示合并后的配对文档。 + +**在 `post-merge` 中或仅通过提交钩子重新生成。** 发生冲突的合并不会运行 `post-merge`,而且该钩子无法改变合并结果。只有暂存区不含未解决条目后才会触发提交钩子,因此仅用钩子无法清除生成文件冲突。 + +**用仓库命令包装每次合并。** 包装命令可以根据已由合并填充的索引解决冲突,显式冲突解决命令也保留这条恢复路径,但直接运行 Git、stack 工具、rebase 和 cherry-pick 时仍会在此前停止。合并驱动才是这些操作共用的文件级扩展点。 + +**通过 GitHub Actions 或应用解决。** 托管自动化可以更新 PR(Pull Request)分支,却会引入凭据、并发控制与分支修改。本地执行或由 agent(智能体)驱动的 merge-forward 工作流已经具有检出目录与推送权限,因此仓库不把远程自动化纳入该机制。 + +## 后果 + +完成安装的 worktree 会自动消除仅由配对记录引起的冲突,同时仍由人工处理配对文档冲突并评判翻译质量。GitHub 托管的可合并性计算不会运行 worktree 本地的可执行命令,因此仍须由贡献者或 agent 将 base 分支合入当前分支并推送结果提交,远端冲突标记才会消失。 + +安装脚本会在 worktree 配置中预留 `merge.dsh-translation-pairing.*`,并拒绝与之冲突的自定义值。与仓库贡献者钩子一样,自动组合依赖已安装的 Node 依赖;运行时不可用时会产生可见的未解决文本结果,而不是选择陈旧元数据。 diff --git a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.i18n.yaml index ecfb0ca0e8..4396748bc7 100644 --- a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.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/simplification/2026-07-29-shared-base-config-overlays.md -2026-07-29-shared-base-config-overlays.md: 9fae9775e0b37acc99ce2eb5fc630e6f4f52bf1d -2026-07-29-shared-base-config-overlays.zh.md: 20cc3e522c369315a1fafc84778fced0260581d0 +2026-07-29-shared-base-config-overlays.md: b2ca5cc8d3a160dd09550827d5f8148c14048ea4 +2026-07-29-shared-base-config-overlays.zh.md: e14aa87bdd6ed3bd711bb9786220b5baf0566e8e diff --git a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md index 9fae9775e0..b2ca5cc8d3 100644 --- a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md +++ b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md @@ -24,7 +24,7 @@ Precedence is list order, last write winning per row: base, then the surface ove A patch replaces its target row's whole `config` rather than merging, which shapes the split: a row whose value differs per surface lives in the overlays, never in the base, so no row is patched by three layers at once. Session identity therefore cannot ride a config key at all — it moved to `dsh-agent-loop`'s `CONFIGURED_AGENT_IDENTITIES_KEY`, as the launcher-owned identity record documented. -`examples/tui-agent`, `examples/cordis-agent`, `examples/code-mode`, and `packages/examples/tui-demo` are deleted. The TUI tests move to `apps/cli/tests/`, the cordis-toolset e2e to `packages/cordis/tool-cordis/tests/`, and the supported Code Mode demo remains the ACP overlay at `examples/acp-agent/code-mode.cordis.yml`. +`examples/tui-agent`, `examples/cordis-agent`, `examples/code-mode`, and `packages/examples/tui-demo` are deleted. The TUI tests move to `apps/cli/tests/`, the cordis-toolset e2e to `packages/self-modification/tool-cordis/tests/`, and the supported Code Mode demo remains the ACP overlay at `examples/acp-agent/code-mode.cordis.yml`. ## Alternatives considered @@ -48,6 +48,6 @@ A patch whose `id` matches no row stays a no-op rather than an error. That is de Composition is checked by booting each tree through the real Loader and inspecting settled entries, not by reading YAML; both surfaces settle with zero unloaded rows, and Web starts its `httpServer` with sandboxed Bash and filesystem providers. Code Mode remains covered by the ACP overlay and programmatic TUI snapshots rather than a separate shipped TUI application. -All eight terminal snapshot scenarios replay byte-identically after moving, and the 14-case PTY smoke passes, including two cases that assert a personal overlay reaches an **inserted** row — the behavior the vendored `plugin-include` fix enables ([`vendor/README.md`](../../../../vendor/README.md) local modification 8, covered by `packages/ui/app-boot/tests/config-reload.spec.ts`). +All eight terminal snapshot scenarios replay byte-identically after moving, and the 14-case PTY smoke passes, including two cases that assert a personal overlay reaches an **inserted** row — the behavior the vendored `plugin-include` fix enables ([`vendor/README.md`](../../../../vendor/README.md) local modification 8, covered by `packages/boot/app-boot/tests/config-reload.spec.ts`). Flattening surfaced three latent defects, each fixed here: the TUI captured the optional `sessionQuery` service once at construction and so could permanently disable `/resume` when it won the mount race; the shipped session-store root silently reverted to a project-local `./.sessions`; and `--config-replace` was dropped by the resume handoff. diff --git a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.zh.md b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.zh.md index 20cc3e522c..e14aa87bdd 100644 --- a/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.zh.md +++ b/.agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.zh.md @@ -24,7 +24,7 @@ Status: implemented patch 会整体替换目标配置项的 `config` 而不合并,这决定了拆分方式:取值因 surface 而异的配置项住在 overlay 中,绝不住在 base 里,从而没有任何配置项会被三层同时 patch。因此会话身份根本不能经由配置键传递——它迁移到了 `dsh-agent-loop` 的 `CONFIGURED_AGENT_IDENTITIES_KEY`,正如启动器持有身份的记录所述。 -`examples/tui-agent`、`examples/cordis-agent`、`examples/code-mode` 与 `packages/examples/tui-demo` 均被删除。TUI 测试迁往 `apps/cli/tests/`,cordis 工具集的 e2e 迁入 `packages/cordis/tool-cordis/tests/`,受支持的 Code Mode demo 则保留为 `examples/acp-agent/code-mode.cordis.yml` 中的 ACP overlay。 +`examples/tui-agent`、`examples/cordis-agent`、`examples/code-mode` 与 `packages/examples/tui-demo` 均被删除。TUI 测试迁往 `apps/cli/tests/`,cordis 工具集的 e2e 迁入 `packages/self-modification/tool-cordis/tests/`,受支持的 Code Mode demo 则保留为 `examples/acp-agent/code-mode.cordis.yml` 中的 ACP overlay。 ## 备选方案 @@ -48,6 +48,6 @@ patch 会整体替换目标配置项的 `config` 而不合并,这决定了拆 组合的正确性通过用真实 Loader 启动每棵树并检查已就绪的条目来核对,而不是靠阅读 YAML:两个界面都能稳定完成且没有未加载项;Web 会以沙箱化 Bash 与文件系统提供方启动 `httpServer`。Code Mode 继续由 ACP overlay 与程序化 TUI 快照覆盖,而不再维护独立交付的 TUI 应用。 -全部八个终端快照场景在迁移后逐字节重放一致,14 个用例的 PTY 冒烟测试全部通过,其中两个用例断言个人 overlay 能触达一个 **insert 进来的**配置项——这正是 vendored `plugin-include` 修复所启用的行为([`vendor/README.md`](../../../../vendor/README.md) 本地修改第 8 条,由 `packages/ui/app-boot/tests/config-reload.spec.ts` 覆盖)。 +全部八个终端快照场景在迁移后逐字节重放一致,14 个用例的 PTY 冒烟测试全部通过,其中两个用例断言个人 overlay 能触达一个 **insert 进来的**配置项——这正是 vendored `plugin-include` 修复所启用的行为([`vendor/README.md`](../../../../vendor/README.md) 本地修改第 8 条,由 `packages/boot/app-boot/tests/config-reload.spec.ts` 覆盖)。 平铺过程暴露出三处潜伏缺陷,均在此一并修复:TUI 曾在构造时一次性捕获可选的 `sessionQuery` 服务,因此在挂载竞争中胜出时会永久禁用 `/resume`;交付的会话存储根目录曾静默退回项目本地的 `./.sessions`;`--config-replace` 曾在 resume 交接中被丢弃。 diff --git a/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml index 70d56bbca5..5ef82c77de 100644 --- a/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.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/testing/2026-06-22-fork-child-replay-seed-boundary.md -2026-06-22-fork-child-replay-seed-boundary.md: ed3ec095bc14128f5ebc0a9188bc022ef97b1c8b -2026-06-22-fork-child-replay-seed-boundary.zh.md: 1d938cbf6c9c32a144d58fed48e552d85aa9c625 +2026-06-22-fork-child-replay-seed-boundary.md: 27db3b768f2503121b0bc8912f9bc0fecb00a10d +2026-06-22-fork-child-replay-seed-boundary.zh.md: f64fc55fc63e2caa4b50112407b645e6e7f12b0f diff --git a/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md index ed3ec095bc..27db3b768f 100644 --- a/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md +++ b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md @@ -44,6 +44,6 @@ This closes the routing correctness gap, and two recorded fork scenarios exercis ## Consequences -- A new persisted header field across core + both backends; the core-data-structures catalog (`persistence.md`) is updated in the same change (its `SessionHeader` / `CreateSessionOptions` `type-equiv` blocks). +- A new persisted header field across core + both backends; the subsystems catalog (`persistence.md`) is updated in the same change (its `SessionHeader` / `CreateSessionOptions` `type-equiv` blocks). - Existing SQLite databases at schema v2 are rejected on open (no user data pre-release). - Spawn replay is unchanged (`seedLength` 0). Fork replay now routes a child to its own script; covered by a regression in `llm-replay`'s tests (a child fixture whose seeded prefix carries a parent chunk — the derived child script must exclude it, proven red without the slice) and a persistence round-trip test (both backends, via the shared coordinator contract). diff --git a/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md index 1d938cbf6c..f64fc55fc6 100644 --- a/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md +++ b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md @@ -44,6 +44,6 @@ subagent 脚本由 [`deriveReplayScript`](../../../../packages/support/llm-repla ## 后果 -- core 与两个后端新增一个持久化 header 字段;核心数据结构目录(`persistence.md`)在同一变更中更新(其 `SessionHeader` / `CreateSessionOptions` 的 `type-equiv` 块)。 +- core 与两个后端新增一个持久化 header 字段;子系统目录(`persistence.md`)在同一变更中更新(其 `SessionHeader` / `CreateSessionOptions` 的 `type-equiv` 块)。 - 既有的 schema v2 SQLite 数据库在打开时被拒绝(预发布阶段无用户数据)。 - spawn 回放不变(`seedLength` 为 0)。fork 回放现在将子会话路由到自身的脚本;由 `llm-replay` 测试中的一个回归用例覆盖(一个子会话 fixture,其播种前缀包含父会话的分片——推导出的子会话脚本必须排除它,不做 slice 时该用例为红)以及一个持久化往返测试(两个后端,通过共享的 coordinator 契约)。 diff --git a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.i18n.yaml index 176097ad5e..8109eb4854 100644 --- a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-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 .agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md -2026-07-24-domain-kv-storage-and-workspace.md: 230877628428dc88dbddeecfe5f4353cf15e151d -2026-07-24-domain-kv-storage-and-workspace.zh.md: cb1fb1346a28b0c3249848705ca209f57cab766e +2026-07-24-domain-kv-storage-and-workspace.md: 1cb2bc4b29c196dbb33292547c75d01a37f06772 +2026-07-24-domain-kv-storage-and-workspace.zh.md: 2904065a7374803495a9bbf8ff4e044310d9bd02 diff --git a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md index 2308776284..1cb2bc4b29 100644 --- a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md +++ b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md @@ -6,7 +6,7 @@ English | [中文](2026-07-24-domain-kv-storage-and-workspace.zh.md) ## Problem -The host's only persistence surface is the session event log (`packages/session-persistence`: append-only, one file per session). Anything that does not belong to a single session has nowhere to live, and two real needs exist today: +The host's only persistence surface is the session event log (`packages/session/session-persistence`: append-only, one file per session). Anything that does not belong to a single session has nowhere to live, and two real needs exist today: - **The workspace entity.** The GUI needs workspace as a real object: path, title, and the list of owned sessions. Ownership belongs to the workspace — "which sessions belong to this workspace" is not any single session's fact, so writing it into the session log is semantically wrong. Until now workspace was only a sidebar visual grouping derived from cwd, with no entity (that conclusion has been overturned). - **Dynamic session metadata** (the foreseeable second consumer). Cold session listings read only the first log line (an immutable creation-time snapshot); title, terminal status, and anything that evolves with the session is unavailable. The fix direction is a sidecar metadata table — exactly a KV table with high-frequency per-key updates. @@ -26,7 +26,7 @@ Create the `packages/storage/` group — the `ctx.storage` hub (backend registry | `@deepseek-ai/dsh-storage-sqlite` | `packages/storage/storage-sqlite/` | registers backend `sqlite` | ✓ | | `@deepseek-ai/dsh-storage-domain` | `packages/storage/storage-domain/` | mounts `ctx.storage.domain` | ✓ | | `@deepseek-ai/dsh-workspace` | `packages/workspace/workspace/` | `ctx.workspace` | ✓ | -| `SessionPersistence.delete` extension + cascade orchestration | `packages/session-persistence/*` | new method on the existing seam | ✗ future work (session side untouched this phase) | +| `SessionPersistence.delete` extension + cascade orchestration | `packages/session/session-persistence*` | new method on the existing seam | ✗ future work (session side untouched this phase) | | `workspace.*` / `session.delete` RPC, GUI wiring, boot assembly | — | — | ✗ next phase | (workspace lives in its own group rather than `packages/host/`: the host group's naming rule requires the `dsh-host-*` prefix while this package is named `dsh-workspace`; and the workspace entity is a domain concept, not bound to the host assembly tier. Unrelated to the existing `workspace-context` package — that is an AGENTS.md instruction loader.) diff --git a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md index cb1fb1346a..2904065a73 100644 --- a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md @@ -6,7 +6,7 @@ Status: proposed ## 问题 -host 侧唯一的持久化面是 session 事件日志(`packages/session-persistence`:仅追加、一 session 一文件)。凡是"不属于某个 session"的信息就没有落盘处,眼下有两个真实需求: +host 侧唯一的持久化面是 session 事件日志(`packages/session/session-persistence`:仅追加、一 session 一文件)。凡是"不属于某个 session"的信息就没有落盘处,眼下有两个真实需求: - **workspace 实体**。GUI 要把 workspace 做成真实对象:路径、标题、关联 session 清单。归属关系由 workspace 持有——"哪些 session 属于这个 workspace"不是任何单个 session 自己的事实,塞进 session log 语义不成立。此前 workspace 只是 sidebar 上按 cwd 分组的视觉概念,没有实体(该结论已被推翻)。 - **session 动态元信息**(可预见的第二个消费方)。冷会话列表只读日志首行 header(创建时的不可变快照),title、结束状态这类随会话推进变化的信息拿不到;补齐方向是 sidecar 元数据表——正是一张按 key 高频点更新的 KV 表。 @@ -26,7 +26,7 @@ host 侧唯一的持久化面是 session 事件日志(`packages/session-persis | `@deepseek-ai/dsh-storage-sqlite` | `packages/storage/storage-sqlite/` | 注册后端 `sqlite` | ✓ | | `@deepseek-ai/dsh-storage-domain` | `packages/storage/storage-domain/` | 挂载 `ctx.storage.domain` | ✓ | | `@deepseek-ai/dsh-workspace` | `packages/workspace/workspace/` | `ctx.workspace` | ✓ | -| `SessionPersistence.delete` 扩面 + 级联删编排 | `packages/session-persistence/*` | 既有 seam 新方法 | ✗ future work(本期不动 session 侧) | +| `SessionPersistence.delete` 扩面 + 级联删编排 | `packages/session/*` | 既有 seam 新方法 | ✗ future work(本期不动 session 侧) | | `workspace.*` / `session.delete` RPC、GUI 接线、boot 组装 | — | — | ✗ 下期 | (workspace 放独立组不放 `packages/host/`:host 组命名规则要求 `dsh-host-*` 前缀,而包名定为 `dsh-workspace`;且 workspace 实体是领域概念,不绑定 host 装配层。与既有 `workspace-context` 包无关——那是 AGENTS.md 指令加载器。) diff --git a/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.i18n.yaml index 7adc61691c..0fe388a3dc 100644 --- a/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.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/proposed/architecture/2026-07-27-session-projection-and-command-log.md -2026-07-27-session-projection-and-command-log.md: 8226325d4cd4047c3a9c41eb9427082c59753dd5 -2026-07-27-session-projection-and-command-log.zh.md: b55704f28799008b5e2323d846696e4768fcd6d6 +2026-07-27-session-projection-and-command-log.md: 2fc463ab6e65998c19b0a0fce186c7eb17dacbf6 +2026-07-27-session-projection-and-command-log.zh.md: 89877d3aabcd074863f912b334c927268014e4d8 diff --git a/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md b/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md index 8226325d4c..2fc463ab6e 100644 --- a/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md +++ b/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md @@ -123,7 +123,7 @@ Two log-only (non-surface, model-invisible) events, mirroring the `tool/call`/`t 'command/done': { commandId: string; kind: 'success' | 'error'; text?: string } ``` -The host command executor (`packages/ui/commands`) appends `command/run` before invoking the handler and `command/done` at settlement — direct standalone appends on the receiving agent's session, in the same shape as every other plugin-owned log-only event after the [synthetic-turn removal](../../implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.md): no turn wraps them (turns describe model-loop executions only), persistence drains them at ordinary checkpoints, and the commands package's own invariant companion enforces the run/done pairing. The payload is structured — `name` and, by default, `args` are the parser's own split (`parseCommand`'s name and rawInput), so a consumer (a projection unit folding its own command records, a rich command card) never re-parses a line. A definition sets `recordInput: false` when its authoritative domain event owns the payload; `command/run` then omits `args` rather than duplicating it. `text` is the handler's verbatim outcome — factual data of the same nature as `tool/result.content`, not presentation (how it is laid out remains client-computed at render time, satisfying the "presentation never enters the log" red line). Domains that want the model to know the outcome keep doing what they do today (plan's narration, goal's inject) — that is a domain decision, unchanged. +The host command executor (`packages/interaction/commands`) appends `command/run` before invoking the handler and `command/done` at settlement — direct standalone appends on the receiving agent's session, in the same shape as every other plugin-owned log-only event after the [synthetic-turn removal](../../implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.md): no turn wraps them (turns describe model-loop executions only), persistence drains them at ordinary checkpoints, and the commands package's own invariant companion enforces the run/done pairing. The payload is structured — `name` and, by default, `args` are the parser's own split (`parseCommand`'s name and rawInput), so a consumer (a projection unit folding its own command records, a rich command card) never re-parses a line. A definition sets `recordInput: false` when its authoritative domain event owns the payload; `command/run` then omits `args` rather than duplicating it. `text` is the handler's verbatim outcome — factual data of the same nature as `tool/result.content`, not presentation (how it is laid out remains client-computed at render time, satisfying the "presentation never enters the log" red line). Domains that want the model to know the outcome keep doing what they do today (plan's narration, goal's inject) — that is a domain decision, unchanged. Because committed events broadcast on the mux stream, refresh persistence, multi-tab sync, and fork/resume recovery all come for free. The `command.execute` RPC degrades to admission — `{ matched, commandId? }`: whether the line resolved, and the minted pairing id when it did, so the issuing client can correlate its request with the flow node the lifecycle events produce. The one-shot notice channel (`runDetached` → `noticeFor`) is retired. diff --git a/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md b/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md index b55704f287..89877d3aab 100644 --- a/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md @@ -123,7 +123,7 @@ type UseProjection = { 'command/done': { commandId: string; kind: 'success' | 'error'; text?: string } ``` -host 侧命令执行器(`packages/ui/commands`)在调用处理器前追加 `command/run`,在结算时追加 `command/done`——在接收 agent 的会话上直接独立追加,与[合成轮次移除](../../implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.md)之后所有插件自有 log-only 事件同一形状:没有轮次包裹它们(轮次只描述模型循环执行),持久化在常规检查点排空它们,run/done 配对由 commands 包自己的 invariant 伴生插件把守。载荷是结构化的——`name` 以及默认携带的 `args` 来自解析器自己的切分(`parseCommand` 的 name 与 rawInput),因此消费方(折叠自己命令记录的投影单元、富命令卡片)永远无需重新解析行文本。当载荷由权威领域事件持有时,命令定义会设置 `recordInput: false`;此时 `command/run` 省略 `args`,而不是重复该载荷。`text` 是处理器的原样结果——与 `tool/result.content` 同一性质的事实数据,不是呈现(版式如何编排仍由客户端在渲染时计算,满足「呈现永不入日志」这条红线)。想让模型知道结果的领域继续做它们今天在做的事(plan 的旁白、goal 的注入)——那是领域自己的决定,保持不变。 +host 侧命令执行器(`packages/interaction/commands`)在调用处理器前追加 `command/run`,在结算时追加 `command/done`——在接收 agent 的会话上直接独立追加,与[合成轮次移除](../../implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.md)之后所有插件自有 log-only 事件同一形状:没有轮次包裹它们(轮次只描述模型循环执行),持久化在常规检查点排空它们,run/done 配对由 commands 包自己的 invariant 伴生插件把守。载荷是结构化的——`name` 以及默认携带的 `args` 来自解析器自己的切分(`parseCommand` 的 name 与 rawInput),因此消费方(折叠自己命令记录的投影单元、富命令卡片)永远无需重新解析行文本。当载荷由权威领域事件持有时,命令定义会设置 `recordInput: false`;此时 `command/run` 省略 `args`,而不是重复该载荷。`text` 是处理器的原样结果——与 `tool/result.content` 同一性质的事实数据,不是呈现(版式如何编排仍由客户端在渲染时计算,满足「呈现永不入日志」这条红线)。想让模型知道结果的领域继续做它们今天在做的事(plan 的旁白、goal 的注入)——那是领域自己的决定,保持不变。 由于已提交事件会在 mux 流上广播,刷新后仍在、多标签页同步、fork/恢复后可还原这三件事随之全部自动获得。`command.execute` RPC 退化为准入判定——`{ matched, commandId? }`:该行是否匹配命中,以及命中时新铸的配对 id,发起命令的客户端据此把自己的请求与生命周期事件产出的 flow 节点关联起来。一次性通知通道(`runDetached` → `noticeFor`)就此下线。 diff --git a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml index f1bd359e19..040d099f38 100644 --- a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.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/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md -2026-07-28-storage-root-and-derived-medium-recovery.md: 2edb60d204da736bf14ace6278fa4ee7b9e4b8f7 -2026-07-28-storage-root-and-derived-medium-recovery.zh.md: 802ca16f399446777203581d246444e53d632a34 +2026-07-28-storage-root-and-derived-medium-recovery.md: c7b423770fcca89c54d3866db7701348351cc259 +2026-07-28-storage-root-and-derived-medium-recovery.zh.md: 54a2bbc6496bd8cce594b3ce3a105e4be6e8ff64 diff --git a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md index 2edb60d204..c7b423770f 100644 --- a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md +++ b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md @@ -8,7 +8,7 @@ English | [中文](2026-07-28-storage-root-and-derived-medium-recovery.zh.md) The persisted projection cache ([RFC](2026-07-27-session-projection-and-command-log.md), shipped as `dsh-session-projection-cache`) surfaced two gaps in the storage substrate it landed on. Both are properties of the domain-KV stack ([design](2026-07-24-domain-kv-storage-and-workspace.md)), not of the cache itself, and both bite the cache first because it is the first *derived* medium on that stack. -**Where the files actually live (root mismatch closed; resolve-once residual still open).** The shared base defaults the session store to the global harness home (`$DSH_HOME/sessions`, default `~/.dsh/sessions`), while the shipped Web overlay used to give the json backend the relative root `./.storages`: `workspace.json` and `session_projcache.json` landed under `/.storages/` — two launches from different directories shared their sessions yet saw different workspace registries and different projection caches, and the cache exists precisely to serve the cross-session cold listing, which missed for every session last cached under another launch directory. That mismatch is now closed: the overlay anchors `storage-json.root` to `$DSH_HOME/storages` with the same `!!js` expression the session root uses (`apps/cli/config/web.cordis.yml`). The residual hazard: `JsonStorageBackend` still never resolves its root — each unit open joins the path against whatever `process.cwd()` is at that moment (packages/storage/storage-json/src/index.ts); the shipped overlay root is already absolute and unaffected, but any relative root (bare Loader boots, tests) still splits on a later cwd change — the exact hazard the JSONL session backend resolves-once to prevent ("later process.cwd() changes cannot split one backend across roots", packages/session-persistence/session-persistence-jsonl/src/index.ts). +**Where the files actually live (root mismatch closed; resolve-once residual still open).** The shared base defaults the session store to the global harness home (`$DSH_HOME/sessions`, default `~/.dsh/sessions`), while the shipped Web overlay used to give the json backend the relative root `./.storages`: `workspace.json` and `session_projcache.json` landed under `/.storages/` — two launches from different directories shared their sessions yet saw different workspace registries and different projection caches, and the cache exists precisely to serve the cross-session cold listing, which missed for every session last cached under another launch directory. That mismatch is now closed: the overlay anchors `storage-json.root` to `$DSH_HOME/storages` with the same `!!js` expression the session root uses (`apps/cli/config/web.cordis.yml`). The residual hazard: `JsonStorageBackend` still never resolves its root — each unit open joins the path against whatever `process.cwd()` is at that moment (packages/storage/storage-json/src/index.ts); the shipped overlay root is already absolute and unaffected, but any relative root (bare Loader boots, tests) still splits on a later cwd change — the exact hazard the JSONL session backend resolves-once to prevent ("later process.cwd() changes cannot split one backend across roots", packages/session/session-persistence-jsonl/src/index.ts). **How recovery works today.** Inside a healthy medium the cache is fully self-healing by design: a `stateVersion`-mismatched row is discarded and refolded, a log shrunk below a row's watermark is detected by the anchored restore floor and answered with one full re-read, and every background write is fail-soft. But at the *medium* level there is no recovery at all: a truncated, hand-edited, or version-bumped `session_projcache.json` fails `openJsonUnit` with `malformed-medium`/`version-mismatch` (packages/storage/storage-json/src/format.ts), a schema-drifted record fails domain open with `invalid-record` (packages/storage/storage-domain/src/index.ts), the rejection propagates through `SessionProjectionCache[Service.init]`, and under the CLI's fail-loud boot the assembly refuses to start. A file whose entire content is rebuildable from session logs can brick boot. This contradicts the cache package's own stated stance ("a stale or unreadable cache costs a longer tail replay, never a wrong value") and the cache domain spec's JSDoc ("version bumps discard the whole medium"), which today describes an aspiration, not the implementation. The same fail-loud path is *correct* for `workspace.json` — workspace records are authoritative, not derivable — so the missing concept is a per-domain declaration of authority, not a global behavior change. diff --git a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md index 802ca16f39..54a2bbc649 100644 --- a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md @@ -8,7 +8,7 @@ Status: proposed 持久投影缓存([RFC](2026-07-27-session-projection-and-command-log.md),已作为 `dsh-session-projection-cache` 落地)暴露了它所依托的存储基座的两个缺口。二者都是 domain-KV 栈([设计](2026-07-24-domain-kv-storage-and-workspace.md))的属性而非缓存自身的问题,且都首先咬到缓存——因为它是这条栈上第一个*派生*介质。 -**文件到底存在哪(根错位已收口,resolve-once 残余仍开放)。** 共享 base 将会话存储默认为全局 harness home(`$DSH_HOME/sessions`,默认 `~/.dsh/sessions`),而出厂 Web overlay 曾给 json 后端相对根 `./.storages`:`workspace.json` 和 `session_projcache.json` 落在 `<启动目录>/.storages/` 下——从两个不同目录启动,会话相同,工作区注册表和投影缓存却各是一份,而缓存存在的意义恰恰是跨会话冷列表,凡上次在别的启动目录下缓存过的会话全部 miss。这一错位已消除:overlay 现以与会话根同一段 `!!js` 表达式把 `storage-json.root` 锚定到 `$DSH_HOME/storages`(`apps/cli/config/web.cordis.yml`)。残余隐患:`JsonStorageBackend` 仍从不 resolve 根——每次打开 unit 都把路径 join 到当时的 `process.cwd()` 上(packages/storage/storage-json/src/index.ts);出厂 overlay 的根已是绝对路径不受影响,但任何相对根(裸 Loader 启动、测试)仍会被后续 cwd 变化劈开,JSONL 会话后端用「构造时 resolve 一次」防住的正是它("later process.cwd() changes cannot split one backend across roots",packages/session-persistence/session-persistence-jsonl/src/index.ts)。 +**文件到底存在哪(根错位已收口,resolve-once 残余仍开放)。** 共享 base 将会话存储默认为全局 harness home(`$DSH_HOME/sessions`,默认 `~/.dsh/sessions`),而出厂 Web overlay 曾给 json 后端相对根 `./.storages`:`workspace.json` 和 `session_projcache.json` 落在 `<启动目录>/.storages/` 下——从两个不同目录启动,会话相同,工作区注册表和投影缓存却各是一份,而缓存存在的意义恰恰是跨会话冷列表,凡上次在别的启动目录下缓存过的会话全部 miss。这一错位已消除:overlay 现以与会话根同一段 `!!js` 表达式把 `storage-json.root` 锚定到 `$DSH_HOME/storages`(`apps/cli/config/web.cordis.yml`)。残余隐患:`JsonStorageBackend` 仍从不 resolve 根——每次打开 unit 都把路径 join 到当时的 `process.cwd()` 上(packages/storage/storage-json/src/index.ts);出厂 overlay 的根已是绝对路径不受影响,但任何相对根(裸 Loader 启动、测试)仍会被后续 cwd 变化劈开,JSONL 会话后端用「构造时 resolve 一次」防住的正是它("later process.cwd() changes cannot split one backend across roots",packages/session/session-persistence-jsonl/src/index.ts)。 **现在是怎么恢复的。** 在健康介质内部,缓存按设计完全自愈:`stateVersion` 不匹配的行被丢弃重折,日志缩短到行水位以下由带锚的 restore floor 检出并以一次全量重读回答,每次后台写都是 fail-soft。但在*介质*层面完全没有恢复:被截断、被手改或版本被 bump 的 `session_projcache.json` 会让 `openJsonUnit` 以 `malformed-medium`/`version-mismatch` 失败(packages/storage/storage-json/src/format.ts),schema 漂移的记录让域 open 以 `invalid-record` 失败(packages/storage/storage-domain/src/index.ts),拒绝一路穿过 `SessionProjectionCache[Service.init]`,在 CLI 的 fail-loud 启动下整个组装拒绝启动。一个内容完全可从会话日志重建的文件能把启动搞死。这与缓存包自己声明的立场("a stale or unreadable cache costs a longer tail replay, never a wrong value")和缓存域 spec 的 JSDoc("version bumps discard the whole medium")相矛盾——后者今天描述的是愿望而非实现。同一条 fail-loud 路径对 `workspace.json` 却是*正确*的——工作区记录是权威数据,不可派生——所以缺的概念是按域声明权威性,而不是全局改行为。 diff --git a/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.i18n.yaml b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.i18n.yaml index b7cfbde315..af8cad7d63 100644 --- a/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.i18n.yaml +++ b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.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/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md -2026-07-04-prune-dead-core-spine-surface.md: 473f655ab0944b43f9b1193413801eb7a80286d8 -2026-07-04-prune-dead-core-spine-surface.zh.md: 3389c0161efa78252c9810d64f46d0cd8a7a034c +2026-07-04-prune-dead-core-spine-surface.md: 7ee7ffc6e8c0f3672c98be6e23ef45a1abfc2b4e +2026-07-04-prune-dead-core-spine-surface.zh.md: b65d607c1736545089c7116f8a14ab6e7510a8e1 diff --git a/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md index 473f655ab0..7ee7ffc6e8 100644 --- a/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md +++ b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md @@ -8,7 +8,7 @@ English | [中文](2026-07-04-prune-dead-core-spine-surface.zh.md) Several package-root exports, result fields, and convenience methods have no production consumer. They survive because tests import internals through public entry points or because a type anticipated a caller that never arrived. Each item is small in isolation, but together they enlarge the SDK contract, generated catalogs, documentation, and regression matrix without enabling a shipped path. -The production corpus is `packages/*/*/src`, example sources/config, and runtime scripts. Tests, package READMEs, and Agent Note prose are evidence of publication but not fixed callers. `cordis_inspect` makes `packages/cordis/tool-cordis/src/api-catalog.ts` model-visible, and `cordis_mount` can invoke injected services through guarded real-service proxies, so catalogued service methods and returned shapes are a genuine dynamic product surface. The table therefore distinguishes absence of a fixed repository caller from unreachability: rows touching catalogued vocabulary intentionally contract what model-written mounts can discover and call, while package-root implementation helpers are not reached through that service façade. Exact-symbol searches produce the following inventory: +The production corpus is `packages/*/*/src`, example sources/config, and runtime scripts. Tests, package READMEs, and Agent Note prose are evidence of publication but not fixed callers. `cordis_inspect` makes `packages/self-modification/tool-cordis/src/api-catalog.ts` model-visible, and `cordis_mount` can invoke injected services through guarded real-service proxies, so catalogued service methods and returned shapes are a genuine dynamic product surface. The table therefore distinguishes absence of a fixed repository caller from unreachability: rows touching catalogued vocabulary intentionally contract what model-written mounts can discover and call, while package-root implementation helpers are not reached through that service façade. Exact-symbol searches produce the following inventory: | Surface | Production evidence | Simplification | | --- | --- | --- | diff --git a/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.zh.md b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.zh.md index 3389c0161e..b65d607c17 100644 --- a/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.zh.md +++ b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.zh.md @@ -8,7 +8,7 @@ Status: proposed 若干包根导出、结果字段和便利方法没有生产消费方。它们之所以存活,要么是因为测试通过公开入口导入了内部实现,要么是因为某个类型预期了一个从未出现的调用者。每一项单独看都很小,但合在一起,它们扩大了 SDK 契约、生成的 catalog、文档和回归矩阵,却没有支撑任何已交付的路径。 -生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、包 README 和 Agent Note 行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/cordis/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此 catalog 中的服务方法和返回形状是真正的动态产品接口。下表因此区分「没有固定的仓库调用者」与「不可达」:涉及 catalog 词汇的行有意收缩模型编写的 mount 能发现和调用的内容,而包根实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单: +生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、包 README 和 Agent Note 行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/self-modification/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此 catalog 中的服务方法和返回形状是真正的动态产品接口。下表因此区分「没有固定的仓库调用者」与「不可达」:涉及 catalog 词汇的行有意收缩模型编写的 mount 能发现和调用的内容,而包根实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单: | 接口 | 生产证据 | 简化方式 | | --- | --- | --- | diff --git a/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml index cdebeeac5c..71d6fa82b5 100644 --- a/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml +++ b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.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/proposed/simplification/2026-07-19-make-jsonrpc-directional.md -2026-07-19-make-jsonrpc-directional.md: 910b4988aca34dec499b2e34cb2a42042c81b0cb -2026-07-19-make-jsonrpc-directional.zh.md: 9810cf758faa078583955ba541e2b2fe9ad9c855 +2026-07-19-make-jsonrpc-directional.md: 945d58a0c989dac2beeb129bfd545fe5674c63cd +2026-07-19-make-jsonrpc-directional.zh.md: 181aab8cd2f2473f0b2694df1dff072fd805e69d diff --git a/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md index 910b4988ac..945d58a0c9 100644 --- a/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md +++ b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md @@ -20,11 +20,11 @@ Return the settled outcome directly from `session/prompt` as `{ status, reason } ## Implementation plan -1. In `packages/ui/jsonrpc/src/server.ts`, replace `SessionPromptResult.accepted` with `status: 'ok' | 'error' | 'aborted'` and the captured `TurnEndReason`. `HarnessSdkServer.prompt()` will return `completed` as `ok`, `aborted` as `aborted`, and every other current or merge-extensible reason as `error`; reaching idle without a `turn/end` remains an invariant error. Remove only `session.finished`, leaving `session.event`, `subagent.started`, and `subagent.finished` unchanged. -2. In `packages/sdk/sdk-protocol/src/transport.ts`, narrow the shared class to the directions with consumers — inbound requests/outbound responses (the server) and outbound requests/inbound responses plus inbound notifications (the TypeScript SDK client) — removing only server-originated `request()` use and client-originated notification dispatch, or split the class into a server-side and client-side transport. Request result, method-not-found, and handler-error responses retain their current behavior and remain ordered after notifications emitted by the awaited handler. +1. In `packages/scaffold/server/src/server.ts`, replace `SessionPromptResult.accepted` with `status: 'ok' | 'error' | 'aborted'` and the captured `TurnEndReason`. `HarnessSdkServer.prompt()` will return `completed` as `ok`, `aborted` as `aborted`, and every other current or merge-extensible reason as `error`; reaching idle without a `turn/end` remains an invariant error. Remove only `session.finished`, leaving `session.event`, `subagent.started`, and `subagent.finished` unchanged. +2. In `packages/scaffold/protocol/src/transport.ts`, narrow the shared class to the directions with consumers — inbound requests/outbound responses (the server) and outbound requests/inbound responses plus inbound notifications (the TypeScript SDK client) — removing only server-originated `request()` use and client-originated notification dispatch, or split the class into a server-side and client-side transport. Request result, method-not-found, and handler-error responses retain their current behavior and remain ordered after notifications emitted by the awaited handler. 3. In `python/sdk/src/deepseek_harness/client.py`, `models.py`, and `__init__.py`, remove `IncomingRequest`, `_requests`, `notify()`, `next_request()`, `respond()`, and `respond_error()`. Add a public validated `SessionPromptResponse` carrying status and reason, return it from `session_prompt()`, and keep an explicit reader guard that ignores unexpected server-request frames instead of allowing them to match a response waiter. 4. In `python/sdk/src/deepseek_harness/api.py`, build `TurnResult.status` and a new `TurnResult.reason` from `SessionPromptResponse`, then delete the `session.finished` branch and second completion loop. Keep the subscription open during the request and preserve `_request_raw()`'s final notification drain so the last `turn/end` event and any subagent notification written before the response are collected before `Session.run()` reconstructs the final assistant message. -5. Replace the symmetric transport-pair cases in `packages/sdk/sdk-protocol/tests/transport.spec.ts` with per-direction coverage, and update `server.spec.ts`, `plugin-apply.spec.ts`, and `built-scope-carrier.e2e.ts` for direct outcomes, ordering, overlap, shutdown, and the narrowed fake; update the TypeScript SDK client (`packages/sdk/sdk-client`) and its suites for response-based settlement. Update `python/sdk/tests/test_client.py` for response-based settlement, unexpected-request-frame handling, callback and concurrency behavior, and the removed public helpers. Update the JSON-RPC and bilingual Python SDK READMEs, export JSDoc and declarations, `scripts/smoke-python-runtime.py`, and the Python single-executable snapshot. +5. Replace the symmetric transport-pair cases in `packages/scaffold/protocol/tests/transport.spec.ts` with per-direction coverage, and update `server.spec.ts`, `plugin-apply.spec.ts`, and `built-scope-carrier.e2e.ts` for direct outcomes, ordering, overlap, shutdown, and the narrowed fake; update the TypeScript SDK client (`packages/scaffold/client`) and its suites for response-based settlement. Update `python/sdk/tests/test_client.py` for response-based settlement, unexpected-request-frame handling, callback and concurrency behavior, and the removed public helpers. Update the JSON-RPC and bilingual Python SDK READMEs, export JSDoc and declarations, `scripts/smoke-python-runtime.py`, and the Python single-executable snapshot. ## Alternatives considered diff --git a/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md index 9810cf758f..181aab8cd2 100644 --- a/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md +++ b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md @@ -20,11 +20,11 @@ JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协 ## 实施计划 -1. 在 `packages/ui/jsonrpc/src/server.ts` 中,用 `status: 'ok' | 'error' | 'aborted'` 和捕获的 `TurnEndReason` 替换 `SessionPromptResult.accepted`。`HarnessSdkServer.prompt()` 把 `completed` 映射为 `ok`,把 `aborted` 映射为 `aborted`,把其他当前已有或可通过声明合并扩展的原因映射为 `error`;进入空闲状态却没有 `turn/end` 仍视为不变量错误。只删除 `session.finished`,保持 `session.event`、`subagent.started` 和 `subagent.finished` 不变。 -2. 在 `packages/sdk/sdk-protocol/src/transport.ts` 中,把共享类收窄到有消费者的方向——入站请求/出站响应(服务端)与出站请求/入站响应及入站通知(TypeScript SDK 客户端)——只删除服务端发起的 `request()` 用法与客户端发起的通知分发,或把该类拆分为服务端与客户端两个传输。请求结果、方法不存在与处理器错误响应保持原有行为,并继续排在被等待处理器发出的通知之后。 +1. 在 `packages/scaffold/server/src/server.ts` 中,用 `status: 'ok' | 'error' | 'aborted'` 和捕获的 `TurnEndReason` 替换 `SessionPromptResult.accepted`。`HarnessSdkServer.prompt()` 把 `completed` 映射为 `ok`,把 `aborted` 映射为 `aborted`,把其他当前已有或可通过声明合并扩展的原因映射为 `error`;进入空闲状态却没有 `turn/end` 仍视为不变量错误。只删除 `session.finished`,保持 `session.event`、`subagent.started` 和 `subagent.finished` 不变。 +2. 在 `packages/scaffold/protocol/src/transport.ts` 中,把共享类收窄到有消费者的方向——入站请求/出站响应(服务端)与出站请求/入站响应及入站通知(TypeScript SDK 客户端)——只删除服务端发起的 `request()` 用法与客户端发起的通知分发,或把该类拆分为服务端与客户端两个传输。请求结果、方法不存在与处理器错误响应保持原有行为,并继续排在被等待处理器发出的通知之后。 3. 在 `python/sdk/src/deepseek_harness/client.py`、`models.py` 和 `__init__.py` 中,删除 `IncomingRequest`、`_requests`、`notify()`、`next_request()`、`respond()` 和 `respond_error()`。新增公开且经过校验的 `SessionPromptResponse` 来携带状态与原因,由 `session_prompt()` 返回该对象,并保留明确的读取保护:忽略意外的服务端请求帧,避免它们命中响应等待器。 4. 在 `python/sdk/src/deepseek_harness/api.py` 中,根据 `SessionPromptResponse` 构造 `TurnResult.status` 和新增的 `TurnResult.reason`,再删除 `session.finished` 分支与第二个完成循环。请求期间保持订阅打开,并保留 `_request_raw()` 最后的通知排空步骤,确保写在响应前的最后一条 `turn/end` 事件与任何 subagent 通知,都会在 `Session.run()` 重建最终助手消息之前被收集。 -5. 用按方向的覆盖替换 `packages/sdk/sdk-protocol/tests/transport.spec.ts` 中的对称传输对用例,并更新 `server.spec.ts`、`plugin-apply.spec.ts` 和 `built-scope-carrier.e2e.ts`,覆盖直接结果、顺序、重叠、关闭和收窄后的伪实现;同步更新 TypeScript SDK 客户端(`packages/sdk/sdk-client`)及其套件以采用基于响应的结束流程。更新 `python/sdk/tests/test_client.py`,覆盖基于响应的结束流程、意外请求帧处理、回调与并发行为,以及已删除的公开辅助方法。同步更新 JSON-RPC README、双语 Python SDK README、导出 JSDoc 与声明、`scripts/smoke-python-runtime.py` 和 Python 单可执行文件快照。 +5. 用按方向的覆盖替换 `packages/scaffold/protocol/tests/transport.spec.ts` 中的对称传输对用例,并更新 `server.spec.ts`、`plugin-apply.spec.ts` 和 `built-scope-carrier.e2e.ts`,覆盖直接结果、顺序、重叠、关闭和收窄后的伪实现;同步更新 TypeScript SDK 客户端(`packages/scaffold/client`)及其套件以采用基于响应的结束流程。更新 `python/sdk/tests/test_client.py`,覆盖基于响应的结束流程、意外请求帧处理、回调与并发行为,以及已删除的公开辅助方法。同步更新 JSON-RPC README、双语 Python SDK README、导出 JSDoc 与声明、`scripts/smoke-python-runtime.py` 和 Python 单可执行文件快照。 ## 备选方案 diff --git a/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.i18n.yaml b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.i18n.yaml index e252ed8190..d9dd98233e 100644 --- a/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.i18n.yaml +++ b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.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/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md -2026-06-20-assembled-assistant-messages-only.md: ba8135a3d63f292cfedd23de8b4b9d43b4455e8c -2026-06-20-assembled-assistant-messages-only.zh.md: 5d29655a0697d05f20e491d28272898a08ec3e0b +2026-06-20-assembled-assistant-messages-only.md: edc756f71271065ec579a2becb8597190b34cdc8 +2026-06-20-assembled-assistant-messages-only.zh.md: e5f4f8a63a35f5f40c482a4a4855937cc8e6d544 diff --git a/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md index ba8135a3d6..edc756f712 100644 --- a/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md +++ b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md @@ -19,7 +19,7 @@ ACP `session/load` can replay prior assistant messages as complete content block ## Acceptance criteria - `SessionEventMap` drops `assistant/chunk`, or marks it as non-persisted if a transitional live event is needed. -- [Session persistence docs](../../../../packages/session-persistence/session-persistence/README.md) no longer require every stream chunk to be stored verbatim. +- [Session persistence docs](../../../../packages/session/session-persistence/README.md) no longer require every stream chunk to be stored verbatim. - `llm-replay` and ACP snapshots use an explicit replay fixture format or sidecar for model chunks. - `session/load` renders completed assistant messages from `assistant/message`. - Stored logs get much smaller and remain `seq`-contiguous without chunk holes. diff --git a/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md index 5d29655a06..e5f4f8a63a 100644 --- a/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md +++ b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md @@ -19,7 +19,7 @@ ACP `session/load` 可以将先前的 assistant 消息作为完整内容块回 ## 验收标准 - `SessionEventMap` 移除 `assistant/chunk`,或在需要过渡性实时事件时将其标记为非持久化。 -- [会话持久化文档](../../../../packages/session-persistence/session-persistence/README.md)不再要求逐字存储每个流式分片。 +- [会话持久化文档](../../../../packages/session/session-persistence/README.md)不再要求逐字存储每个流式分片。 - `llm-replay` 和 ACP 快照使用显式的回放 fixture 格式或伴随文件来存储模型分片。 - `session/load` 从 `assistant/message` 渲染已完成的 assistant 消息。 - 存储的日志大幅缩小,且删除分片后仍保持 `seq` 连续,不留下序号缺口。 diff --git a/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.i18n.yaml b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.i18n.yaml index 0ae596bf03..4c5b2d04de 100644 --- a/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.i18n.yaml +++ b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.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/rejected/simplification/2026-06-20-fold-session-persistence-interface.md -2026-06-20-fold-session-persistence-interface.md: 895b868b2a80d8655284bae1364a85e19e174da7 -2026-06-20-fold-session-persistence-interface.zh.md: d5ce0ed4532706ffadb43460cd0974507393a5ed +2026-06-20-fold-session-persistence-interface.md: c0da160e766cf0bed532f0d11bd52e3a027dbe8b +2026-06-20-fold-session-persistence-interface.zh.md: 93ab244429600bd15e2d8dadfa7dc218e37c2eff diff --git a/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md index 895b868b2a..c0da160e76 100644 --- a/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md +++ b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md @@ -22,7 +22,7 @@ The implementing PR should update the [capability seams](../../implemented/archi - `dsh-session` exports the persistence service type, coordinator, and contract helpers. - JSONL and SQLite backend packages depend on `dsh-session` directly. - `agent-loop` resume uses the session-owned service key. -- [Session persistence](../../implemented/architecture/2026-06-14-session-persistence.md), [shared persistence write coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md), and [package docs](../../../../packages/session-persistence/session-persistence/README.md) explain why backend implementations remain separate. +- [Session persistence](../../implemented/architecture/2026-06-14-session-persistence.md), [shared persistence write coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md), and [package docs](../../../../packages/session/session-persistence/README.md) explain why backend implementations remain separate. ## What we give up diff --git a/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.zh.md b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.zh.md index d5ce0ed453..93ab244429 100644 --- a/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.zh.md +++ b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.zh.md @@ -22,7 +22,7 @@ Status: rejected — 独立的持久化接口包是为持久化后端设计的 - `dsh-session` 导出持久化服务类型、协调器和契约辅助工具。 - JSONL 和 SQLite 后端包直接依赖 `dsh-session`。 - `agent-loop` 的恢复功能使用会话包拥有的服务键。 -- [会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)、[共享持久化写入协调器](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)与[包文档](../../../../packages/session-persistence/session-persistence/README.md)说明后端实现为何仍保持独立。 +- [会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)、[共享持久化写入协调器](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)与[包文档](../../../../packages/session/session-persistence/README.md)说明后端实现为何仍保持独立。 ## 放弃了什么 diff --git a/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.i18n.yaml b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.i18n.yaml index 09f42a9047..7e9b79fdeb 100644 --- a/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.i18n.yaml +++ b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.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/rejected/simplification/2026-06-20-truncate-interrupted-turns.md -2026-06-20-truncate-interrupted-turns.md: 3c7acf8d673568f851edd52635a28f73d9bf5f6f -2026-06-20-truncate-interrupted-turns.zh.md: f348e6a99d8214f7968d9f57931f9dddd37973be +2026-06-20-truncate-interrupted-turns.md: c4b0084de537927a27829eb78990c6559bd1df7d +2026-06-20-truncate-interrupted-turns.zh.md: 2d3eca74416eff360029e3ac77f04662878a0084 diff --git a/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md index 3c7acf8d67..c4b0084de5 100644 --- a/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md +++ b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md @@ -21,7 +21,7 @@ This makes the persisted turn boundary simple: a completed `turn/end` is the che - `TurnEndReasonMap` drops the `interrupted` variant. - `interruptedTurnClosers()` and its tests disappear. - The persistence coordinator's repair hook truncates backend-specific torn/open tail state without appending closers. -- [Session persistence docs](../../../../packages/session-persistence/session-persistence/README.md) say load returns the last completed turn, plus no partial final turn. +- [Session persistence docs](../../../../packages/session/session-persistence/README.md) say load returns the last completed turn, plus no partial final turn. - Snapshot and contract tests update together with the behavior they pin. - The session format version and recorded fixtures are refreshed; non-current stored logs are rejected per the pre-release format policy, with no migration path. diff --git a/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.zh.md b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.zh.md index f348e6a99d..2d3eca7441 100644 --- a/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.zh.md +++ b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.zh.md @@ -21,7 +21,7 @@ Status: rejected — 单个轮次可以包含大量真实工作,包括多个 - `TurnEndReasonMap` 移除 `interrupted` 变体。 - `interruptedTurnClosers()` 及其测试删除。 - 持久化协调器的修复钩子截断后端特有的撕裂或未关闭的尾部状态,不追加关闭事件。 -- [会话持久化文档](../../../../packages/session-persistence/session-persistence/README.md)说明加载返回最后一个已完成的轮次,不包含部分最终轮次。 +- [会话持久化文档](../../../../packages/session/session-persistence/README.md)说明加载返回最后一个已完成的轮次,不包含部分最终轮次。 - 快照与契约测试随其所固定的行为一同更新。 - 会话格式版本与记录的 fixture(测试前置数据)刷新;按预发布格式策略,非当前版本的存储日志被拒绝,不提供迁移路径。 diff --git a/.agents/skills/dsh-code-review/SKILL.md b/.agents/skills/dsh-code-review/SKILL.md index f933beb86b..c8a52f5d50 100644 --- a/.agents/skills/dsh-code-review/SKILL.md +++ b/.agents/skills/dsh-code-review/SKILL.md @@ -21,7 +21,7 @@ description: Use when reviewing a pull request in the deepseek-harness repo — 1. **New prose receives semantic review.** Use [dsh-prose-standard](../dsh-prose-standard/SKILL.md) to critically review every added or changed Markdown passage, JSDoc, comment, prompt, description, diagnostic, and visible string. Verify required coverage, accuracy, placement, and editorial quality against the owning code or behavior; automated checks do not establish those properties. 2. **Docs match the code.** Config, defaults, errors, wire fields, events, and public behavior update the package README and JSDoc in the same diff. Comments state non-obvious contracts; flag implementation narration, test walkthroughs, review history, and duplicated rationale for deletion or a link to their one home. -3. **Core type docs match.** Changes to spine or seam vocabulary update the appropriate [core-data-structures](../../../docs/core-data-structures/core.md) page and any `type-equiv` entry. Internal types need no catalog entry. +3. **Core type docs match.** Changes to spine or seam vocabulary update the appropriate [subsystems](../../../docs/subsystems/README.md) page and any `type-equiv` entry. Internal types need no catalog entry. 4. **Registrations clean up.** Verify each new registry contribution satisfies the disposal-test contract in [packages/AGENTS.md](../../../packages/AGENTS.md). 5. **Invariant companions are semantic.** For every touched `./invariant`, require an owner event-stream or mutable-data relationship at its authoritative boundary; service or method presence, plugin metadata or effects, and fixed pure examples belong in type, load, or unit tests. Accept an empty installer when its package-specific reason establishes that no plausible runtime relationship exists; do not demand an invented check merely to eliminate emptiness ([repository rule](../../../AGENTS.md#conventions); [package contract](../../../packages/AGENTS.md)). 6. **Required evidence exists.** Verify the author ran the [relevant local checks](../../../AGENTS.md#run-relevant-checks-locally) for the diff and that CI covers the exhaustive matrix; review the semantic gaps neither can detect. diff --git a/.gitattributes b/.gitattributes index 2efafac8b8..e0a432111d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,3 +7,7 @@ * text=auto eol=lf *.pdf binary + +# Pairing records are generated from the three confirmed owner-blob pairs. +# The worktree-local installer registers the fail-closed driver command. +*.i18n.yaml merge=dsh-translation-pairing diff --git a/.github/workflows/e2b-e2e.yml b/.github/workflows/e2b-e2e.yml new file mode 100644 index 0000000000..abbd0482ed --- /dev/null +++ b/.github/workflows/e2b-e2e.yml @@ -0,0 +1,58 @@ +name: E2E (E2B sandbox) + +# This suite provisions external E2B sandboxes and is intentionally opt-in. +# It has no push, pull_request, schedule, or workflow_call trigger. +on: + workflow_dispatch: + +permissions: + contents: read + +env: + # CI runs must never report to the production telemetry endpoint baked + # into apps/cli/cordis.yml (AppCLIEntry disables the row when set). + DSH_TELEMETRY_DISABLED: '1' + +jobs: + e2b: + runs-on: ubuntu-latest + name: E2B live Loader composition + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + + - name: Install (immutable) + run: pnpm install --frozen-lockfile + + # The tests self-skip locally when the credential is absent. A manually + # dispatched run must fail instead of reporting an all-skipped green. + - name: Preflight (require E2B API key) + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY_EXTERNAL }} + run: | + set -euo pipefail + if [ -z "${E2B_API_KEY:-}" ]; then + echo "::error::E2B_API_KEY is empty. Configure the E2B_API_KEY_EXTERNAL repository secret." + exit 1 + fi + echo "E2B_API_KEY present." + + # The Loader smoke runs package exports under plain Node in lib mode. + - name: Build (lib for the E2B Loader smoke) + run: pnpm run build + + - name: E2B tests (live sandbox) + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY_EXTERNAL }} + DSH_E2E_MAX_WORKERS: '1' + DSH_EXAMPLE_MODE: lib + run: >- + pnpm exec vitest run --config vitest.e2e.config.ts + packages/e2b/e2b/tests/composition.e2e.ts diff --git a/AGENTS.md b/AGENTS.md index 929cb8504e..628415da4a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,21 +25,23 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// web/ web seam + search/fetch providers + model-facing web tools compact/ compaction seam + basic backend context/ request-context plugins - subagent/ subagent seam + spawn/fork/ACP backends + delegation tool - bundle/ profile plugin bundles: installable patch layers for dsh --profile + subagent/ subagent seam + spawn/fork/ACP backends + tool + bundle/ installable dsh --profile patch-layer bundles workflow/ workflow seam + worker-thread engine + workflow tool todo/ todo_write tool - plan/ plan mode as logged per-agent collaboration state + plan/ plan mode as logged state preset/ per-session agent composition from preset cordis.yml files - guard/ loop-hygiene plugins - cordis/ self-referential toolset: the agent inspects/mounts plugins in its own runtime - hooks/ Claude Code/Codex hook bridges + shared wire-protocol library - session-persistence/ persistence seam + JSONL/SQLite backends - settings/ user-settings seam + file-backed provider - credentials/ credential-reference seam + env-over-.env provider + guard/ loop-hygiene + tool-timeout plugins + self-modification/ the agent inspects/mounts its own plugins + hooks/ Claude Code/Codex hook bridges + wire-protocol library + session/ durable session data: persistence, projection, titles, telemetry + settings/ user-settings seam + file provider + credentials/ credential-reference seam + env/.env provider acp/ automation-only Agent Client Protocol server - ui/ JSON-RPC bridge; boot, approval, interaction plugins - examples/ demo bundles (agent-spine + CLI/ACP/JSON-RPC bins) leaves load + interaction/ approval/interaction seams, permission, commands, ask-user + boot/ shared app-bin glue + scaffold/ project tooling: helper, launcher, initializer, SDK protocol + examples/ demo bundles (agent-spine + CLI/ACP/JSON-RPC bins) support/ dev/test infrastructure util/ zero-dependency utilities python/ Python SDK and bundled runtime (see python/README.md) diff --git a/README.i18n.yaml b/README.i18n.yaml index c4ad1f0fd9..077f5fa7f2 100644 --- a/README.i18n.yaml +++ b/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 README.md -README.md: 64f06c0aec0905fa7deabbec0deea61e1c7a40d4 -README.zh.md: fee03118926028833c828809764ebb5f6375259e +README.md: ff4cf661a060772973629844d43758d26ac1be02 +README.zh.md: 977de7f22244ff49579393edcce66d02c5782de4 diff --git a/README.md b/README.md index 64f06c0aec..ff4cf661a0 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Built-in capabilities cover file reading, editing, and search; shell and persist - **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design. - **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log). - **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode). -- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/cordis/tool-cordis/README.md). +- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/self-modification/tool-cordis/README.md). ## Community diff --git a/README.zh.md b/README.zh.md index fee0311892..977de7f222 100644 --- a/README.zh.md +++ b/README.zh.md @@ -76,7 +76,7 @@ pnpm run demo:acp - **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均可组合为 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。 - **运行可重建。** 凡是模型可见的内容,都会记录在权威会话流中;持久化、恢复/fork/查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。 - **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。 -- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/cordis/tool-cordis/README.md)。 +- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/self-modification/tool-cordis/README.md)。 ## 社区 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 27e391320a..325de8db4e 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/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 apps/cli/reference/README.md -README.md: 496cecdb64e3254a2a77690f55f760b4cd90b521 -README.zh.md: 4673bf764347307a9b91e2a5474a8439cf67b481 +README.md: e616b420d60e0fa372bf38950207245cb60ced98 +README.zh.md: a65140e6dc9374e21bdd3018bce45432794a7b10 diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 496cecdb64..e616b420d6 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -69,7 +69,7 @@ The base bundle mounts the native DeepSeek adapter, settings and credential prov Session events stream as OTLP/HTTP logs by default. `DSH_TELEMETRY_OTLP_URL` selects another collector. Any non-empty `DSH_TELEMETRY_DISABLED` disables the telemetry row before boot. The shipped base has no telemetry redaction rule, so exported records can contain message text, tool arguments and results, and workspace paths; the [telemetry Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md) owns that deployment decision. -The empty `repository-plugins` row lets profile patch layers mount prepared immutable repository Plugin generations. See the [repository Plugin contract](../../../packages/cordis/repository-plugin/README.md#standalone-app-configuration). The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox. +The empty `repository-plugins` row lets profile patch layers mount prepared immutable repository Plugin generations. See the [repository Plugin contract](../../../packages/self-modification/repository-plugin/README.md#standalone-app-configuration). The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox. ## Source launcher diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index 4673bf7643..a65140e6dc 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -69,7 +69,7 @@ dsh web --dump-config 会话事件默认作为 OTLP/HTTP 日志流式发送。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。任何非空 `DSH_TELEMETRY_DISABLED` 都会在启动前禁用遥测配置行。随附基础配置没有遥测脱敏规则,因此导出的记录可能包含消息文本、工具参数与结果以及 workspace 路径;该部署决策由[遥测 Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md)负责。 -空 `repository-plugins` 行让 profile 的 patch 层能够挂载已准备的不可变 repository Plugin generation。参见 [repository Plugin 契约](../../../packages/cordis/repository-plugin/README.md#standalone-app-configuration)。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。 +空 `repository-plugins` 行让 profile 的 patch 层能够挂载已准备的不可变 repository Plugin generation。参见 [repository Plugin 契约](../../../packages/self-modification/repository-plugin/README.md#standalone-app-configuration)。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。 ## 源码启动器 diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 36c4bad6dd..d830e8fba6 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -18,7 +18,7 @@ "path": "../../vendor/include" }, { - "path": "../../packages/ui/app-boot" + "path": "../../packages/boot/app-boot" }, { "path": "../../packages/bundle/base" diff --git a/docs/AGENTS.md b/docs/AGENTS.md index cd4e8aaaa5..a5eca656db 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -20,24 +20,26 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home. |---|---|---| | Root `AGENTS.md` | Standing orders: rules an agent needs in context in every session, one to three lines each, linking its home | Stories, worked examples, situational procedures, anything restated from a linked home | | Subtree `AGENTS.md` (`packages/`, `examples/`, `docs/`, `.agents/notes/`) | Orders specific to that subtree | Repo-wide rules the root file already carries | -| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ core-data-structures), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations | -| [core-data-structures/](core-data-structures/core.md) | The type catalog: literal shapes and semantics of the spine and seam vocabulary | Behavior narration (→ architecture.md) | -| [Agent Notes](../.agents/notes/README.md) | Decision records under their own lifecycle contract | Migration plans, checklists, and spec-speak once implemented; archived notes are frozen history | +| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ subsystems), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations | +| [subsystems/](subsystems/README.md) | One reference page per subsystem: type shapes, semantics, and the generated Cordis surface | Behavior narration (→ architecture.md) | +| [Agent Notes](../.agents/notes/README.md) | Active decision records: the why, what-was-given-up, and verification contract; `implemented/` notes describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped; archived notes are frozen history, never current authority | | [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — | | [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the Agent Note each guide links) | | [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history | -| 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 | +| Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns | +| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), gate-by-gate enumerations that drift from `package.json` scripts | +| Generated reference: the per-page `cordis-surface` regions in [subsystems/](subsystems/README.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) | +Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type shapes → subsystems; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link. + ## Writing rules - **Document current state, not change history.** Avoid "previously/now/no longer", PRs, commits, and stack positions in durable prose; name the live mechanism. Put change stories in commits, PRs, Agent Notes, or postmortems. - **Every non-trivial change includes at least one Agent Note in the same PR.** Update the owning note or add one; only mechanical/local edits are exempt ([scope](../.agents/notes/README.md#when-to-write-one)). - **One physical line per paragraph** (`verify-md-wrap`): use editor soft-wrap. Code blocks, tables, and list structure keep their formatting; code comments stay under the linter's column limit. - **Fenced `ts` blocks must compile** (`doc-typecheck`); a pasted type declaration and its original JSDoc use ` ```ts type-equiv `, while a body-stripped public class declaration uses ` ```ts public-api `; register either in the manifest so neither can drift ([mechanics](development.md#documenting-types-verbatim-ts-type-equiv)). -- **The [core-data-structures catalog](core-data-structures/core.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types ([what counts as core](core-data-structures/core.md#what-counts-as-core)). +- **The owning [subsystems page](subsystems/README.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types; a type is documented on its declaring package group's page ([page scoping](../.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md)). - **Bilingual pairs update together**: editing either side obligates the counterpart and a re-record in the same change ([i18n contract](i18n/README.md)). - **Comments and JSDoc state complete contracts, not reasoning transcripts.** Preserve behavior, timing, modality, exceptions, consequences, and non-obvious orientation; delete narration, test walkthroughs, review analysis, and code restatement. Keep the local contract and link its rationale. Use [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for details. - Your audience is professional programmers. Prefer concise and straight-forward English over metaphor. Do not overuse words like "gate", "vocabulary", "surface", "seams". diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 23e1cce712..4a74d511d8 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: 832928f773133e8f2e674e7e3fa97f83b7a25664 -architecture.zh.md: b6c1ff41090bfa4cd9455657a71f751ac8c17687 +architecture.md: 33452a0827f8032e29844bd5fd9fbec2135cae41 +architecture.zh.md: 99a33cbbf17761964758a167805c431817c49f67 diff --git a/docs/architecture.md b/docs/architecture.md index 832928f773..33452a0827 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -41,9 +41,9 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services, | `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry, generic `task_*` controls | | `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration | | `ctx.goals` | [`goal/`](../packages/goal/README.md) | persisted same-session goals | -| `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | durable session-log storage | +| `ctx.sessionPersistence` | [`session/`](../packages/session/README.md) | durable session-log storage | | `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | live-preferred exact/filter/trace queries over SQLite FTS, workspace-authorized model tools | -| `ctx.sessionTitle` | [`session-title/`](../packages/session-title/README.md) | log-backed fallbacks, one optional asynchronous provider | +| `ctx.sessionTitle` | [`session/session-title`](../packages/session/README.md) | log-backed fallbacks, one optional asynchronous provider | | `ctx.settings` | [`settings/`](../packages/settings/README.md) | per-plugin user-settings namespaces layered over composition entries | | `ctx.credentials` | [`credentials/`](../packages/credentials/README.md) | named secret references resolved per operation, never inlined in configuration | | `ctx.directoryPicker` | [`host/directory-picker`](../packages/host/directory-picker/README.md) | GUI-host directory picking (`native`/`browse` interactions) | @@ -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 @@ -123,7 +123,7 @@ Adapter selection, dispatch, and iteration failures become terminal error or abo Other failures use `agent/error`; cancellation and disposal beat recovery. Before request-header commit, the turn signal cancels capability preparation; undispatched tools get synthetic `tool/call`/`ABORTED_BEFORE_DISPATCH` pairs. Effective `cancel(cause)` reports its cause before clearing and aborting; idle calls emit nothing. Waking input that lands after the abort fires but before convergence runs at the driver's convergence boundary, while a `disposed` cancel leaves it parked ([cancel-convergence wake latch](../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)). Durability distinguishes `aborted` cancellation from `disposed` teardown, which awaits quiescence ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)). -Turn and step events are turn-enclosed; the loop appends `user/message` events only from entered batches inside a turn. A turn opens before the initial claim and pre-step, so rejection, empty input, cancellation, or failure closes a durable turn without any step events. Standalone `compact/* { turn: null }` events consume no turn, and their lock-time markers may interleave with inbox splices. Reload synthesizes interrupted turn ends; `session/end-seed` distinguishes stale compaction orphans from live locks. After close, only `agent/error` reports failures. Each turn has one [TurnEndReason](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap). +Turn and step events are turn-enclosed; the loop appends `user/message` events only from entered batches inside a turn. A turn opens before the initial claim and pre-step, so rejection, empty input, cancellation, or failure closes a durable turn without any step events. Standalone `compact/* { turn: null }` events consume no turn, and their lock-time markers may interleave with inbox splices. Reload synthesizes interrupted turn ends; `session/end-seed` distinguishes stale compaction orphans from live locks. After close, only `agent/error` reports failures. Each turn has one [TurnEndReason](subsystems/session.md#why-a-turn-ended-turnendreasonmap). ### Agent Handles @@ -147,9 +147,9 @@ Between turns, owners append log-only events through `Session`, flushing only fo ### Model Content -Messages use typed blocks from merge-extensible `ContentBlockMap`; the pattern also types `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`. New blocks coordinate adapters, UI, compaction, token metering, and persistence; replay measurements live in [token-meter.md](core-data-structures/token-meter.md). +Messages use typed blocks from merge-extensible `ContentBlockMap`; the pattern also types `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`. New blocks coordinate adapters, UI, compaction, token metering, and persistence; replay measurements live in [token-meter.md](subsystems/token-meter.md). -Streaming uses raw chunks and `BlockAssembler`. Each `LlmAdapter.stream()` is one provider attempt; adapters report normalized failure facts, and a handling `agent/request-error` plugin returns a retry action. The loop logs chunks, successful provenance, and replay state. Remote adapters use per-read idle watchdogs. Replay crosses routes only through a shared adapter instance ([contract](core-data-structures/llm-streaming.md)). +Streaming uses raw chunks and `BlockAssembler`. Each `LlmAdapter.stream()` is one provider attempt; adapters report normalized failure facts, and a handling `agent/request-error` plugin returns a retry action. The loop logs chunks, successful provenance, and replay state. Remote adapters use per-read idle watchdogs. Replay crosses routes only through a shared adapter instance ([contract](subsystems/llm-streaming.md)). ## Extension And Composition @@ -157,13 +157,13 @@ Streaming uses raw chunks and `BlockAssembler`. Each `LlmAdapter.stream()` is on Capabilities separate **interface / implementation / consumer** layers. Filesystem and subprocess providers define one execution world; Bash, PTY, and LSP run there without provider forks. See the [capability graph](capability-seams.md). -Exceptions combine LLM interface/consumer, filesystem policy, web registries, and named skill/subagent providers. Subagents spawn fresh, fork a completed-turn prefix, use ACP children, or delegate one self-contained turn to a real product provider such as Codex ([subagent.md](core-data-structures/subagent.md)). +Exceptions combine LLM interface/consumer, filesystem policy, web registries, and named skill/subagent providers. Subagents spawn fresh, fork a completed-turn prefix, use ACP children, or delegate one self-contained turn to a real product provider such as Codex ([subagent.md](subsystems/subagent.md)). `dsh-workspace-context` composes its baseline on the first `agent/pre-step` and folds it into the final entering batch right after the claimed prompt, so it reaches the first request with the direct prompt; rejection keeps it in the next-step inbox. When compaction removes that baseline from the visible surface, the next entering pre-step composes the current baseline and carries it in the same request. Filesystem changes projected after tools are likewise folded into the next entering pre-step instead of creating a later context-only step ([decision](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md)). `dsh-paths` owns shared paths. ### Bundles And Apps -`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC front doors ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [ui/](../packages/ui/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)). +`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC front doors ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [interaction/](../packages/interaction/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)). ### Agent Presets diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index b6c1ff4109..99a33cbbf1 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -41,9 +41,9 @@ | `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | 后台任务注册表和通用 `task_*` 控制 | | `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | 脚本驱动的多 agent 编排 | | `ctx.goals` | [`goal/`](../packages/goal/README.md) | 持久化的同会话目标 | -| `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | 会话日志的持久化存储 | +| `ctx.sessionPersistence` | [`session/`](../packages/session/README.md) | 会话日志的持久化存储 | | `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | 基于 SQLite 全文搜索的实时优先精确检索/过滤/追踪、经工作区授权的模型工具 | -| `ctx.sessionTitle` | [`session-title/`](../packages/session-title/README.md) | 基于日志的回退标题和单个可选异步提供方 | +| `ctx.sessionTitle` | [`session/session-title`](../packages/session/README.md) | 基于日志的回退标题和单个可选异步提供方 | | `ctx.settings` | [`settings/`](../packages/settings/README.md) | 按插件划分的用户设置命名空间,分层叠加在装配条目之上 | | `ctx.credentials` | [`credentials/`](../packages/credentials/README.md) | 具名密钥引用,按操作解析,绝不内联进配置 | | `ctx.directoryPicker` | [`host/directory-picker`](../packages/host/directory-picker/README.md) | GUI 宿主目录选取(`native`/`browse` 交互) | @@ -53,7 +53,7 @@ ## 事件 -事件就是服务的扩展 API([目录](cordis-catalog/events.md)、[生产方与消费方映射](event-producer-consumer.md))。 +事件就是服务的扩展 API([子系统](subsystems/core.md)、[生产方与消费方映射](event-producer-consumer.md))。 ### 事件域 @@ -123,7 +123,7 @@ idle inject: 其他故障使用 `agent/error`;取消和资源释放优先于恢复。在提交请求头之前,轮次信号会取消功能准备;尚未分派的工具会得到合成的 `tool/call`/`ABORTED_BEFORE_DISPATCH` 对。实际生效的 `cancel(cause)` 会在清空队列和中止前报告原因;空闲调用不发事件。abort 触发后、收敛前到达的唤醒输入会在 driver 的收敛边界执行,而 `disposed` 取消则将其停放([取消收敛窗口唤醒锁存](../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md))。持久化层以 `aborted` 区分取消,以 `disposed` 区分会等待完全停稳的拆卸([决策](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md))。 -轮次和步骤事件均位于轮次边界内;loop 只会在轮次内从进入步骤的批次追加 `user/message`。轮次会在首次领取与 pre-step 之前打开,因此拒绝、空输入、取消或失败会关闭一个不包含任何步骤事件的持久轮次。独立的 `compact/* { turn: null }` 事件不占用轮次,其锁定时刻标记可以与 inbox splice 交错。重新加载会为中断的轮次合成结束事件;`session/end-seed` 区分陈旧的压缩遗留项与活跃锁。关闭后仅由 `agent/error` 报告故障。每个轮次有一个 [TurnEndReason](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap)。 +轮次和步骤事件均位于轮次边界内;loop 只会在轮次内从进入步骤的批次追加 `user/message`。轮次会在首次领取与 pre-step 之前打开,因此拒绝、空输入、取消或失败会关闭一个不包含任何步骤事件的持久轮次。独立的 `compact/* { turn: null }` 事件不占用轮次,其锁定时刻标记可以与 inbox splice 交错。重新加载会为中断的轮次合成结束事件;`session/end-seed` 区分陈旧的压缩遗留项与活跃锁。关闭后仅由 `agent/error` 报告故障。每个轮次有一个 [TurnEndReason](subsystems/session.md#why-a-turn-ended-turnendreasonmap)。 ### Agent 句柄 @@ -147,9 +147,9 @@ idle inject: ### 模型内容 -消息使用从可合并扩展的 `ContentBlockMap` 派生的类型化块;同一模式也为 `MessageSource`、`FinishReason`、`TurnTrigger` 和 `TurnEndReason` 定义类型。新增块会协调适配器、UI、压缩、token 计量和持久化;回放计量见 [token-meter.md](core-data-structures/token-meter.md)。 +消息使用从可合并扩展的 `ContentBlockMap` 派生的类型化块;同一模式也为 `MessageSource`、`FinishReason`、`TurnTrigger` 和 `TurnEndReason` 定义类型。新增块会协调适配器、UI、压缩、token 计量和持久化;回放计量见 [token-meter.md](subsystems/token-meter.md)。 -流式输出使用原始分片和 `BlockAssembler`。每次 `LlmAdapter.stream()` 调用代表一次提供方尝试;适配器报告标准化的故障事实,负责处理的 `agent/request-error` 插件会返回重试动作。循环会记录分片、成功结果的来源信息和回放状态。远程适配器使用逐次读取空闲看门狗。回放仅通过共用的适配器实例跨路由传递([契约](core-data-structures/llm-streaming.md))。 +流式输出使用原始分片和 `BlockAssembler`。每次 `LlmAdapter.stream()` 调用代表一次提供方尝试;适配器报告标准化的故障事实,负责处理的 `agent/request-error` 插件会返回重试动作。循环会记录分片、成功结果的来源信息和回放状态。远程适配器使用逐次读取空闲看门狗。回放仅通过共用的适配器实例跨路由传递([契约](subsystems/llm-streaming.md))。 ## 扩展与组合 @@ -157,13 +157,13 @@ idle inject: 能力分为**接口/实现/消费方**三层。文件系统与进程管理提供方共同定义一个执行世界;Bash、PTY 和 LSP 都在其中运行,无需提供方专用 fork。参见[功能图](capability-seams.md)。 -例外情况包括 LLM(大语言模型)合并接口和消费方、文件系统整合策略、web 使用注册表、skill 和 subagent 使用具名提供方。subagent 可以通过 spawn 创建全新实例、fork 一个已完成轮次的前缀、使用 ACP(Agent Client Protocol)子 agent,或将一个独立完整的轮次委派给 Codex 等真实产品提供方([subagent.md](core-data-structures/subagent.md))。 +例外情况包括 LLM(大语言模型)合并接口和消费方、文件系统整合策略、web 使用注册表、skill 和 subagent 使用具名提供方。subagent 可以通过 spawn 创建全新实例、fork 一个已完成轮次的前缀、使用 ACP(Agent Client Protocol)子 agent,或将一个独立完整的轮次委派给 Codex 等真实产品提供方([subagent.md](subsystems/subagent.md))。 `dsh-workspace-context` 在第一次 `agent/pre-step` 组合基线并将它折入最终进入的批次、紧随已领取的直接提示词之后,使其与直接提示词一同抵达第一次请求;reject 则将它留在 next-step inbox。当压缩从可见表层移除该基线时,下一次进入步骤的 pre-step 会组合当前基线,并在同一请求中携带它。工具执行后投影的文件系统变更也会折入下一次进入步骤的 pre-step,而不会另外创建稍后的纯上下文步骤([决策](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md))。`dsh-paths` 负责共享路径。 ### 组合包与应用 -`dsh-agent-spine-demo` 组合一套主干和可选目标。应用包负责 CLI(命令行界面)、ACP 自动化入口和 JSON-RPC 入口([README](../packages/examples/agent-spine-demo/README.md)、[acp/](../packages/acp/README.md)、[ui/](../packages/ui/README.md))。`dsh-jsonrpc-agent` 启动外部 `cordis.yml`;Python SDK 在配置缺失时提供默认项([Python SDK](../python/README.md))。轻量部署使用可替换后端和可选工具([examples/](../examples/AGENTS.md)、[可运行接线](cookbook/extension-cookbook.md#runnable-wirings)、[图谱](graph-atlas.md))。 +`dsh-agent-spine-demo` 组合一套主干和可选目标。应用包负责 CLI(命令行界面)、ACP 自动化入口和 JSON-RPC 入口([README](../packages/examples/agent-spine-demo/README.md)、[acp/](../packages/acp/README.md)、[interaction/](../packages/interaction/README.md))。`dsh-jsonrpc-agent` 启动外部 `cordis.yml`;Python SDK 在配置缺失时提供默认项([Python SDK](../python/README.md))。轻量部署使用可替换后端和可选工具([examples/](../examples/AGENTS.md)、[可运行接线](cookbook/extension-cookbook.md#runnable-wirings)、[图谱](graph-atlas.md))。 ### Agent Preset diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 6fbe858330..bfa37d6d76 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -377,28 +377,28 @@ flowchart LR | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. | | `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. | -| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. | +| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. | | `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. | | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. | | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. | -| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session-persistence/session-persistence) | [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. | +| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. | | `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-local`](../packages/settings/settings-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer. | | `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the web gateway exposes value-free views and write-only storage. | -| `ctx.telemetry` | `seam` | [`session-telemetry`](../packages/telemetry/session-telemetry) | [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel) | - | - | The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process. | +| `ctx.telemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process. | | `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json), [`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | Backends register side by side under names; data forms (domain first) mount on the hub and translate typed operations into opaque KV-unit primitives. | | `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace) | - | Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state. | | `ctx.workspace` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections. | | `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. | | `ctx.sessionReferences` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. | -| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session-title/session-title) | [`session-title-first-message-llm`](../packages/session-title/session-title-first-message-llm), [`session-title-all-messages-llm`](../packages/session-title/session-title-all-messages-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. | +| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm), [`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. | | `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. | -| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. | -| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | - | [`tool-ask-user`](../packages/ui/tool-ask-user) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. | +| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/self-modification/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. | +| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/interaction/user-interaction) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. | | `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions. | | `ctx.agentPresets` | `core` | [`agent-presets`](../packages/preset/agent-presets) | - | - | - | Discovers preset directories over trusted and user-authored roots and mounts one preset cordis.yml under an agent scope during creation, rejecting a row that never activates or that publishes into the root service realm. | -| `ctx.commands` | `core` | [`commands`](../packages/ui/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. | -| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session-projection/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session-title/session-title), [`host-apiproxy`](../packages/host/apiproxy) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values. | -| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session-projection/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. | +| `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. | +| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session/session-title), [`host-apiproxy`](../packages/host/apiproxy) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values. | +| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. | | `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. | | `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. | | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. | @@ -411,7 +411,7 @@ flowchart LR | `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox), [`pty-local`](../packages/pty/pty-local) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. | | `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/bash/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox), [`pty-local`](../packages/pty/pty-local) | - | The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots. | | `ctx.approval` | `seam` | `approval` | [`acp`](../packages/acp/acp) | [`tools`](../packages/core/tools), [`tool-bash`](../packages/bash/tool-bash) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. | -| `ctx.permission` | `core` | [`permission`](../packages/ui/permission) | - | - | - | User-facing preset table (`workspace-write`/`danger-full-access`) bundling the sandbox-mode and approval-policy knobs; a switch writes one `permission/preset` event through to both knob events. | +| `ctx.permission` | `core` | [`permission`](../packages/interaction/permission) | - | - | - | User-facing preset table (`workspace-write`/`danger-full-access`) bundling the sandbox-mode and approval-policy knobs; a switch writes one `permission/preset` event through to both knob events. | | `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | [`tools`](../packages/core/tools) | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for Code Mode). | | `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate. | | `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; a model-facing compact tool remains deferred. | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index a1d71a04d9..da7d22c69e 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 [core-data-structures/](core-data-structures/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. @@ -76,7 +76,7 @@ export interface Config { } ``` -Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) +Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) Source: [`packages/examples/acp-demo/src/index.ts:39`](../packages/examples/acp-demo/src/index.ts) @@ -106,7 +106,7 @@ export interface Config { } ``` -Depends on: [`AgentOptions`](core-data-structures/core.md) · [`SessionId`](core-data-structures/core.md) +Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md) Source: [`packages/core/agent-loop/src/index.ts:236`](../packages/core/agent-loop/src/index.ts) @@ -670,7 +670,7 @@ export interface JsonRpcConfig { Depends on: `Readable` (`node:stream`) · `Writable` (`node:stream`) -Source: [`packages/ui/jsonrpc/src/index.ts:25`](../packages/ui/jsonrpc/src/index.ts) +Source: [`packages/scaffold/server/src/index.ts:29`](../packages/scaffold/server/src/index.ts) ## `@deepseek-ai/dsh-llm-deepseek` @@ -1075,9 +1075,9 @@ export interface PresetSpec { } ``` -Depends on: [`ApprovalPolicy`](core-data-structures/approval.md) · [`SandboxMode`](core-data-structures/sandbox.md) +Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsystems/sandbox.md) -Source: [`packages/ui/permission/src/index.ts:140`](../packages/ui/permission/src/index.ts) +Source: [`packages/interaction/permission/src/index.ts:140`](../packages/interaction/permission/src/index.ts) ## `@deepseek-ai/dsh-persona` @@ -1231,7 +1231,7 @@ export interface Config { } ``` -Source: [`packages/cordis/repository-plugin/src/index.ts:42`](../packages/cordis/repository-plugin/src/index.ts) +Source: [`packages/self-modification/repository-plugin/src/index.ts:42`](../packages/self-modification/repository-plugin/src/index.ts) ## `@deepseek-ai/dsh-sandbox-local` @@ -1283,7 +1283,7 @@ export interface Config { } ``` -Depends on: [`SandboxMode`](core-data-structures/sandbox.md) +Depends on: [`SandboxMode`](subsystems/sandbox.md) Source: [`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts) @@ -1322,7 +1322,7 @@ export interface Config { export type JsonlCompression = 'zstd' | 'none' ``` -Source: [`packages/session-persistence/session-persistence-jsonl/src/index.ts:59`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) +Source: [`packages/session/session-persistence-jsonl/src/index.ts:59`](../packages/session/session-persistence-jsonl/src/index.ts) ## `@deepseek-ai/dsh-session-persistence-sqlite` @@ -1365,7 +1365,7 @@ export interface Config { export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' ``` -Source: [`packages/session-persistence/session-persistence-sqlite/src/index.ts:67`](../packages/session-persistence/session-persistence-sqlite/src/index.ts) +Source: [`packages/session/session-persistence-sqlite/src/index.ts:67`](../packages/session/session-persistence-sqlite/src/index.ts) ## `@deepseek-ai/dsh-session-projection-cache` @@ -1386,7 +1386,7 @@ export interface Config { } ``` -Source: [`packages/session-projection/session-projection-cache/src/index.ts:42`](../packages/session-projection/session-projection-cache/src/index.ts) +Source: [`packages/session/session-projection-cache/src/index.ts:42`](../packages/session/session-projection-cache/src/index.ts) ## `@deepseek-ai/dsh-session-query-sqlite` @@ -1486,7 +1486,7 @@ export enum TelemetryMode { Depends on: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`) -Source: [`packages/telemetry/session-telemetry-otel/src/index.ts:80`](../packages/telemetry/session-telemetry-otel/src/index.ts) +Source: [`packages/session/session-telemetry-otel/src/index.ts:80`](../packages/session/session-telemetry-otel/src/index.ts) ## `@deepseek-ai/dsh-session-title` @@ -1504,7 +1504,7 @@ export interface Config { } ``` -Source: [`packages/session-title/session-title/src/index.ts:79`](../packages/session-title/session-title/src/index.ts) +Source: [`packages/session/session-title/src/index.ts:79`](../packages/session/session-title/src/index.ts) ## `@deepseek-ai/dsh-session-title-all-messages-llm` @@ -1515,9 +1515,9 @@ Requires: `sessionTitle` · `llm` · `sessions` export type Config = SessionTitleLlmConfig ``` -Depends on: [`SessionTitleLlmConfig`](../packages/session-title/session-title-llm/src/index.ts) +Depends on: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/index.ts) -Source: [`packages/session-title/session-title-all-messages-llm/src/index.ts:15`](../packages/session-title/session-title-all-messages-llm/src/index.ts) +Source: [`packages/session/session-title-all-messages-llm/src/index.ts:15`](../packages/session/session-title-all-messages-llm/src/index.ts) ## `@deepseek-ai/dsh-session-title-first-message-llm` @@ -1528,9 +1528,9 @@ Requires: `sessionTitle` · `llm` · `sessions` export type Config = SessionTitleLlmConfig ``` -Depends on: [`SessionTitleLlmConfig`](../packages/session-title/session-title-llm/src/index.ts) +Depends on: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/index.ts) -Source: [`packages/session-title/session-title-first-message-llm/src/index.ts:15`](../packages/session-title/session-title-first-message-llm/src/index.ts) +Source: [`packages/session/session-title-first-message-llm/src/index.ts:15`](../packages/session/session-title-first-message-llm/src/index.ts) ## `@deepseek-ai/dsh-settings-local` @@ -2004,7 +2004,7 @@ export interface Config { } ``` -Source: [`packages/cordis/tool-cordis/src/index.ts:25`](../packages/cordis/tool-cordis/src/index.ts) +Source: [`packages/self-modification/tool-cordis/src/index.ts:25`](../packages/self-modification/tool-cordis/src/index.ts) ## `@deepseek-ai/dsh-tool-fs` @@ -2242,7 +2242,7 @@ export interface Config { } ``` -Depends on: [`AgentOptions`](core-data-structures/core.md) +Depends on: [`AgentOptions`](subsystems/core.md) Source: [`packages/subagent/tool-subagent/src/index.ts:25`](../packages/subagent/tool-subagent/src/index.ts) @@ -2261,7 +2261,7 @@ export interface Config { } ``` -Depends on: [`SubagentReportDelivery`](core-data-structures/subagent.md) +Depends on: [`SubagentReportDelivery`](subsystems/subagent.md) Source: [`packages/subagent/tool-subagent-report/src/index.ts:22`](../packages/subagent/tool-subagent-report/src/index.ts) @@ -2414,7 +2414,7 @@ export interface Config { export type ApprovalPolicy = 'ask' | 'never' ``` -Source: [`packages/ui/user-approval/src/index.ts:178`](../packages/ui/user-approval/src/index.ts) +Source: [`packages/interaction/user-approval/src/index.ts:178`](../packages/interaction/user-approval/src/index.ts) ## `@deepseek-ai/dsh-web` @@ -2652,7 +2652,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-command-compact` — requires `commands` · `compact` ([`packages/compact/command-compact/src/index.ts`](../packages/compact/command-compact/src/index.ts)) - `@deepseek-ai/dsh-command-feedback` — requires `commands` ([`packages/feedback/command-feedback/src/index.ts`](../packages/feedback/command-feedback/src/index.ts)) - `@deepseek-ai/dsh-command-goal` — requires `commands` · `goals` ([`packages/goal/command-goal/src/index.ts`](../packages/goal/command-goal/src/index.ts)) -- `@deepseek-ai/dsh-commands` ([`packages/ui/commands/src/index.ts`](../packages/ui/commands/src/index.ts)) +- `@deepseek-ai/dsh-commands` ([`packages/interaction/commands/src/index.ts`](../packages/interaction/commands/src/index.ts)) - `@deepseek-ai/dsh-fs-e2b` — requires `e2b` ([`packages/e2b/fs-e2b/src/index.ts`](../packages/e2b/fs-e2b/src/index.ts)) - `@deepseek-ai/dsh-fs-policy` ([`packages/fs/fs-policy/src/index.ts`](../packages/fs/fs-policy/src/index.ts)) - `@deepseek-ai/dsh-goal-session` — requires `agents` · `goals` · `sessions` ([`packages/goal/goal-session/src/index.ts`](../packages/goal/goal-session/src/index.ts)) @@ -2662,17 +2662,17 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-lsp` ([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts)) - `@deepseek-ai/dsh-pty` ([`packages/pty/pty/src/index.ts`](../packages/pty/pty/src/index.ts)) - `@deepseek-ai/dsh-session` ([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts)) -- `@deepseek-ai/dsh-session-checkpoint-policy` — requires `llm` · `sessionPersistence` · `sessions` · `tools` ([`packages/session-persistence/session-checkpoint-policy/src/index.ts`](../packages/session-persistence/session-checkpoint-policy/src/index.ts)) -- `@deepseek-ai/dsh-session-projection` ([`packages/session-projection/session-projection/src/index.ts`](../packages/session-projection/session-projection/src/index.ts)) +- `@deepseek-ai/dsh-session-checkpoint-policy` — requires `llm` · `sessionPersistence` · `sessions` · `tools` ([`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts)) +- `@deepseek-ai/dsh-session-projection` ([`packages/session/session-projection/src/index.ts`](../packages/session/session-projection/src/index.ts)) - `@deepseek-ai/dsh-skill-badge` — requires `skills` ([`packages/skill/skill-badge/src/index.ts`](../packages/skill/skill-badge/src/index.ts)) - `@deepseek-ai/dsh-storage` ([`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts)) - `@deepseek-ai/dsh-subagent` ([`packages/subagent/subagent/src/index.ts`](../packages/subagent/subagent/src/index.ts)) - `@deepseek-ai/dsh-subprocess-local` ([`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts)) - `@deepseek-ai/dsh-tasks-local` ([`packages/tasks/tasks-local/src/index.ts`](../packages/tasks/tasks-local/src/index.ts)) -- `@deepseek-ai/dsh-timeout-policy` — requires `tools` ([`packages/timeout/timeout-policy/src/index.ts`](../packages/timeout/timeout-policy/src/index.ts)) -- `@deepseek-ai/dsh-tool-ask-user` — requires `tools` · `userInteraction` ([`packages/ui/tool-ask-user/src/index.ts`](../packages/ui/tool-ask-user/src/index.ts)) +- `@deepseek-ai/dsh-timeout-policy` — requires `tools` ([`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts)) +- `@deepseek-ai/dsh-tool-ask-user` — requires `tools` · `userInteraction` ([`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts)) - `@deepseek-ai/dsh-tool-subagent-control` — requires `tools` · `subagents` ([`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts)) -- `@deepseek-ai/dsh-user-interaction` ([`packages/ui/user-interaction/src/index.ts`](../packages/ui/user-interaction/src/index.ts)) +- `@deepseek-ai/dsh-user-interaction` ([`packages/interaction/user-interaction/src/index.ts`](../packages/interaction/user-interaction/src/index.ts)) - `@deepseek-ai/dsh-workspace` — requires `storageDomain` · `sessionPersistence` ([`packages/workspace/workspace/src/index.ts`](../packages/workspace/workspace/src/index.ts)) ## Seam packages (not directly loadable) @@ -2686,7 +2686,7 @@ Abstract service classes — a deployment loads a concrete implementation packag - `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts)) - `@deepseek-ai/dsh-host-directory-picker` — abstract `DirectoryPicker` ([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts)) - `@deepseek-ai/dsh-sandbox` — abstract `SandboxProvider` ([`packages/sandbox/sandbox/src/index.ts`](../packages/sandbox/sandbox/src/index.ts)) -- `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session-persistence/session-persistence/src/index.ts`](../packages/session-persistence/session-persistence/src/index.ts)) +- `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session/session-persistence/src/index.ts`](../packages/session/session-persistence/src/index.ts)) - `@deepseek-ai/dsh-session-query` — abstract `SessionQueryService` ([`packages/session-query/session-query/src/index.ts`](../packages/session-query/session-query/src/index.ts)) - `@deepseek-ai/dsh-settings` — abstract `Settings` ([`packages/settings/settings/src/index.ts`](../packages/settings/settings/src/index.ts)) - `@deepseek-ai/dsh-spill` — abstract `SpillStore` ([`packages/spill/spill/src/index.ts`](../packages/spill/spill/src/index.ts)) @@ -2698,10 +2698,10 @@ Abstract service classes — a deployment loads a concrete implementation packag Imported as libraries by other packages; a `cordis.yml` cannot load them. -- `@deepseek-ai/create-sdk` ([`packages/sdk/create-sdk/src/index.ts`](../packages/sdk/create-sdk/src/index.ts)) +- `@deepseek-ai/create-sdk` ([`packages/scaffold/create-sdk/src/index.ts`](../packages/scaffold/create-sdk/src/index.ts)) - `@deepseek-ai/dsh-acp-snapshot` ([`packages/support/acp-snapshot/src/index.ts`](../packages/support/acp-snapshot/src/index.ts)) - `@deepseek-ai/dsh-agent-loop-testkit` ([`packages/support/agent-loop-testkit/src/index.ts`](../packages/support/agent-loop-testkit/src/index.ts)) -- `@deepseek-ai/dsh-app-boot` ([`packages/ui/app-boot/src/index.ts`](../packages/ui/app-boot/src/index.ts)) +- `@deepseek-ai/dsh-app-boot` ([`packages/boot/app-boot/src/index.ts`](../packages/boot/app-boot/src/index.ts)) - `@deepseek-ai/dsh-atomic-write` ([`packages/util/atomic-write/src/index.ts`](../packages/util/atomic-write/src/index.ts)) - `@deepseek-ai/dsh-base` ([`packages/bundle/base/src/index.ts`](../packages/bundle/base/src/index.ts)) - `@deepseek-ai/dsh-brand` ([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts)) @@ -2712,7 +2712,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-client-web` ([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts)) - `@deepseek-ai/dsh-client-web-react` ([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts)) - `@deepseek-ai/dsh-environment` ([`packages/util/environment/src/index.ts`](../packages/util/environment/src/index.ts)) -- `@deepseek-ai/dsh-helper` ([`packages/sdk/helper/src/index.ts`](../packages/sdk/helper/src/index.ts)) +- `@deepseek-ai/dsh-helper` ([`packages/scaffold/helper/src/index.ts`](../packages/scaffold/helper/src/index.ts)) - `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts)) - `@deepseek-ai/dsh-jsonrpc-demo` ([`packages/examples/jsonrpc-demo/src/index.ts`](../packages/examples/jsonrpc-demo/src/index.ts)) - `@deepseek-ai/dsh-llm-mock-server` ([`packages/support/llm-mock-server/src/index.ts`](../packages/support/llm-mock-server/src/index.ts)) @@ -2721,13 +2721,13 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-paths` ([`packages/util/paths/src/index.ts`](../packages/util/paths/src/index.ts)) - `@deepseek-ai/dsh-retention` ([`packages/util/retention/src/index.ts`](../packages/util/retention/src/index.ts)) - `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts)) -- `@deepseek-ai/dsh-scripts` ([`packages/sdk/scripts/src/index.ts`](../packages/sdk/scripts/src/index.ts)) -- `@deepseek-ai/dsh-sdk-client` ([`packages/sdk/sdk-client/src/index.ts`](../packages/sdk/sdk-client/src/index.ts)) -- `@deepseek-ai/dsh-sdk-protocol` ([`packages/sdk/sdk-protocol/src/index.ts`](../packages/sdk/sdk-protocol/src/index.ts)) -- `@deepseek-ai/dsh-session-telemetry` ([`packages/telemetry/session-telemetry/src/index.ts`](../packages/telemetry/session-telemetry/src/index.ts)) -- `@deepseek-ai/dsh-session-title-llm` ([`packages/session-title/session-title-llm/src/index.ts`](../packages/session-title/session-title-llm/src/index.ts)) +- `@deepseek-ai/dsh-scripts` ([`packages/scaffold/scripts/src/index.ts`](../packages/scaffold/scripts/src/index.ts)) +- `@deepseek-ai/dsh-sdk-client` ([`packages/scaffold/client/src/index.ts`](../packages/scaffold/client/src/index.ts)) +- `@deepseek-ai/dsh-sdk-protocol` ([`packages/scaffold/protocol/src/index.ts`](../packages/scaffold/protocol/src/index.ts)) +- `@deepseek-ai/dsh-session-telemetry` ([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts)) +- `@deepseek-ai/dsh-session-title-llm` ([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts)) - `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts)) -- `@deepseek-ai/dsh-telemetry` ([`packages/sdk/telemetry/src/index.ts`](../packages/sdk/telemetry/src/index.ts)) +- `@deepseek-ai/dsh-telemetry` ([`packages/scaffold/telemetry/src/index.ts`](../packages/scaffold/telemetry/src/index.ts)) - `@deepseek-ai/dsh-timeout` ([`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts)) - `@deepseek-ai/dsh-type-meta` ([`packages/typert/type-meta/src/index.ts`](../packages/typert/type-meta/src/index.ts)) - `@deepseek-ai/dsh-typert-generator` ([`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts)) 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 7015d39bc8..0000000000 --- a/docs/cordis-catalog/events.md +++ /dev/null @@ -1,1097 +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 [core-data-structures/](../core-data-structures/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](../core-data-structures/core.md) · [Scoped](../core-data-structures/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](../core-data-structures/core.md) · [Scoped](../core-data-structures/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](../core-data-structures/core.md) · [Scoped](../core-data-structures/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](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/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](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/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](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/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](../core-data-structures/core.md) · [PreStepDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/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](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/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](../core-data-structures/core.md) · [LlmFailure](../core-data-structures/llm-streaming.md) · [RequestErrorAction](../core-data-structures/core.md) · [ResolvedRetryPolicy](../core-data-structures/llm-streaming.md) · [Scoped](../core-data-structures/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](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/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](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/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](../core-data-structures/core.md) · [Scoped](../core-data-structures/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](../core-data-structures/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](../core-data-structures/approval.md) · [ApprovalRequest](../core-data-structures/approval.md) · [ApprovalService](../core-data-structures/approval.md) · [Scoped](../core-data-structures/scope.md) - -Source: [`packages/ui/user-approval/src/index.ts:30`](../../packages/ui/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/ui/commands/src/index.ts:172`](../../packages/ui/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](../core-data-structures/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 -``` - -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](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/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](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/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](../core-data-structures/filesystem.md) · [FsWriteIntent](../core-data-structures/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](../core-data-structures/core.md) · [GoalChanged](../core-data-structures/goal.md) · [Scoped](../core-data-structures/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](../core-data-structures/core.md) · [LlmService](../core-data-structures/llm-streaming.md) · [StreamChunk](../core-data-structures/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](../core-data-structures/scope.md) · [Session](../core-data-structures/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](../core-data-structures/scope.md) · [Session](../core-data-structures/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](../core-data-structures/scope.md) · [Session](../core-data-structures/session.md) · [SessionEvent](../core-data-structures/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](../core-data-structures/scope.md) · [Session](../core-data-structures/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](../core-data-structures/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](../core-data-structures/settings.md) · [SettingsUpdateSource](../core-data-structures/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](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/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](../core-data-structures/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](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/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](../core-data-structures/system-prompt.md) · [Scoped](../core-data-structures/scope.md) · [SystemPrompt](../core-data-structures/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 -``` - -Source: [`packages/telemetry/session-telemetry/src/index.ts:43`](../../packages/telemetry/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](../core-data-structures/tools.md) · [ContentBlock](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [ToolRegistry](../core-data-structures/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](../core-data-structures/scope.md) · [ToolDispatchExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/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](../core-data-structures/tools.md) · [Scoped](../core-data-structures/scope.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/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](../core-data-structures/tools.md) · [Scoped](../core-data-structures/scope.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/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](../core-data-structures/scope.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/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](../core-data-structures/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](../core-data-structures/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](../core-data-structures/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](../core-data-structures/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](../core-data-structures/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](../core-data-structures/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:22`](../../vendor/hmr/src/index.ts)) -- `hmr/config-update-failed` — A watched config-file refresh failed. ([`vendor/hmr/src/index.ts:29`](../../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 172fd3a3d6..0000000000 --- a/docs/cordis-catalog/services.md +++ /dev/null @@ -1,2996 +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 [core-data-structures/](../core-data-structures/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](../core-data-structures/core.md) · [AgentOptions](../core-data-structures/core.md) · [SessionHeader](../core-data-structures/persistence.md) · [SessionId](../core-data-structures/core.md) - -Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts) - -## `ctx.agentPresets` — `AgentPresets` - -Registry over the deployment's agent presets. - -Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a preset authored while the process runs is visible immediately, and a preset deleted underneath a picker disappears from the next read. - -```ts cordis-catalog -/** - * Every preset the configured roots currently supply. - * @returns the presets, first-root-wins per id. - */ -async list(): Promise - -/** - * Resolve one preset by id. - * @param id - the preset id, or `undefined` for {@link defaultId}. - * @returns the resolved preset. - * @throws when no configured root supplies that id. - */ -async resolve(id?: string): Promise - -/** - * Compose one agent from a preset: ensure the preset's standing mount, then - * parent the agent's scope key to it so the mount's registrations and - * listeners cover this agent. - * - * Call from the agent factory's `setup(agentCtx)`; a rejection there rolls - * the agent creation back, so a broken preset never yields a half-composed - * session. - * @param agentCtx - the agent's scope context. - * @param id - the preset id, or `undefined` for {@link defaultId}. - * @returns the preset that was composed, for the caller to record. - * @throws when the preset is unknown or its composition is unusable. - */ -async mount(agentCtx: Context, id?: string): Promise - -/** - * One agent's instance of a service its preset mounted. - * - * A preset publishes services behind `isolate` realms, which are invisible - * outside the group that declares them — including to the host. This is how a - * caller holding the agent reads one anyway: a request that is ABOUT a - * session but arrives from outside it, which is every browser RPC. - * - * Read addressing only. A host row that `inject`s a service cannot use this, - * because injection resolves before any session exists and has no agent to - * key by; such a service belongs on the host plane instead. - * @param agent - the agent whose composition to look inside. - * @param name - the service name as the preset's rows resolve it. - * @returns the agent's instance, or undefined when its preset mounts none. - */ -serviceFor(agent: { ctx: Context }, name: K): Context[K] | undefined - -/** - * Re-link one agent to a different preset's standing composition. - * - * Only valid while the agent has produced nothing: swapping tools mid - * conversation would leave logged tool calls the new composition cannot - * make. The CALLER owns that check — this method does not read session - * history. - * - * The swap is a parent re-link, not an unmount: standing mounts are shared - * and permanent, so the old composition stays for its other agents and the - * new one is ensured BEFORE the link moves. An unknown or unusable preset - * therefore throws with the agent exactly as it was — there is no torn-down - * state to restore. - * @param agentCtx - the agent's scope context. - * @param id - the preset to compose the agent from instead. - * @returns the preset now installed. - * @throws when the preset is unknown or its composition is unusable. - */ -async recompose(agentCtx: Context, id: string): Promise - -/** - * The standing scope key of one preset, for a host reader with no agent. - * - * A cold transcript read resolves tool presenters against the composition - * the session recorded, and the standing mount makes that possible without - * resuming anything: ensuring the mount composes plugins but starts no - * agent, no session, and no turn. - * @param id - the preset id, or `undefined` for {@link defaultId}. - * @returns the standing scope key readers pass as a registry view scope. - * @throws when the preset is unknown or its composition is unusable. - */ -async standingKeyFor(id?: string): Promise -``` - -Types: [ScopeKey](../core-data-structures/scope.md) - -Source: [`packages/preset/agent-presets/src/index.ts:68`](../../packages/preset/agent-presets/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](../core-data-structures/core.md) · [SessionId](../core-data-structures/core.md) - -Source: [`packages/core/agent/src/index.ts:254`](../../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](../core-data-structures/core.md) · [ApprovalOutcome](../core-data-structures/approval.md) · [ApprovalPolicy](../core-data-structures/approval.md) · [ApprovalRequest](../core-data-structures/approval.md) · [Session](../core-data-structures/session.md) - -Source: [`packages/ui/user-approval/src/index.ts:193`](../../packages/ui/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](../core-data-structures/bash.md) · [BashExecSpec](../core-data-structures/bash.md) · [BashProcess](../core-data-structures/bash.md) · [BashRunResult](../core-data-structures/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](../core-data-structures/subprocess.md) · [ToolExecution](../core-data-structures/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 -``` - -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](../core-data-structures/code-runtime.md) · [CodeRunResult](../core-data-structures/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](../core-data-structures/core.md) · [CommandDefinition](../core-data-structures/commands.md) · [CommandDescriptor](../core-data-structures/commands.md) - -Source: [`packages/ui/commands/src/index.ts:305`](../../packages/ui/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](../core-data-structures/compaction.md) · [CompactionTrigger](../core-data-structures/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](../core-data-structures/credentials.md) · [CredentialRef](../core-data-structures/credentials.md) · [ResolvedCredential](../core-data-structures/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](../core-data-structures/filesystem.md) · [FsEditOutcome](../core-data-structures/filesystem.md) · [FsEditRequest](../core-data-structures/filesystem.md) · [FsInfo](../core-data-structures/filesystem.md) · [FsPathInfo](../core-data-structures/filesystem.md) · [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md) · [FsWriteIntent](../core-data-structures/filesystem.md) · [FsWriteOutcome](../core-data-structures/filesystem.md) · [SandboxExecutionPolicy](../core-data-structures/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](../core-data-structures/core.md) · [CreateGoalRequest](../core-data-structures/goal.md) · [CreateGoalResult](../core-data-structures/goal.md) · [EditGoalRequest](../core-data-structures/goal.md) · [GoalBlockReason](../core-data-structures/goal.md) · [GoalRef](../core-data-structures/goal.md) · [GoalView](../core-data-structures/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 -``` - -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 -``` - -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](../core-data-structures/core.md) · [DirectoryRegistrationHandle](../core-data-structures/core.md) · [GenerateOptions](../core-data-structures/core.md) · [LlmAdapter](../core-data-structures/llm-streaming.md) · [LlmCallConfig](../core-data-structures/core.md) · [LlmConfigurableProvider](../core-data-structures/core.md) · [LlmDiscoveredModel](../core-data-structures/core.md) · [LlmModelDiscoveryRequest](../core-data-structures/core.md) · [LlmModelInfo](../core-data-structures/core.md) · [LlmProviderInfo](../core-data-structures/core.md) · [LlmResolvedModelInfo](../core-data-structures/core.md) · [PreparedLlmCall](../core-data-structures/llm-streaming.md) · [ResolvedRetryPolicy](../core-data-structures/llm-streaming.md) · [StreamChunk](../core-data-structures/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: [Session](../core-data-structures/session.md) · [SessionEvent](../core-data-structures/core.md) - -Source: [`packages/ui/permission/src/index.ts:159`](../../packages/ui/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](../core-data-structures/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](../core-data-structures/core.md) · [PtyBackend](../core-data-structures/pty.md) · [PtyReadRequest](../core-data-structures/pty.md) · [PtyReadResult](../core-data-structures/pty.md) · [PtySendOperation](../core-data-structures/pty.md) · [PtySendRequest](../core-data-structures/pty.md) · [PtySessionId](../core-data-structures/pty.md) · [PtySessionSnapshot](../core-data-structures/pty.md) · [PtySignal](../core-data-structures/pty.md) · [PtySignalResult](../core-data-structures/pty.md) · [PtySpawnRequest](../core-data-structures/pty.md) · [PtySpawnResult](../core-data-structures/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](../core-data-structures/sandbox.md) · [SandboxPolicy](../core-data-structures/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](../core-data-structures/sandbox.md) · [SandboxMode](../core-data-structures/sandbox.md) · [SandboxPolicyRequest](../core-data-structures/sandbox.md) · [Session](../core-data-structures/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](../core-data-structures/core.md) · [SessionHeader](../core-data-structures/persistence.md) · [SessionId](../core-data-structures/core.md) · [SessionInspection](../core-data-structures/persistence.md) · [SessionLocation](../core-data-structures/persistence.md) · [SessionPersistenceSnapshot](../core-data-structures/persistence.md) · [SessionPreparation](../core-data-structures/persistence.md) - -Source: [`packages/session-persistence/session-persistence/src/index.ts:72`](../../packages/session-persistence/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](../core-data-structures/session.md) · [SessionHeader](../core-data-structures/persistence.md) · [SessionId](../core-data-structures/core.md) - -Source: [`packages/session-projection/session-projection-cache/src/index.ts:71`](../../packages/session-projection/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. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads. - -```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](../core-data-structures/session.md) · [SessionEvent](../core-data-structures/core.md) - -Source: [`packages/session-projection/session-projection/src/index.ts:171`](../../packages/session-projection/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](../core-data-structures/session-query.md) · [SessionEventRecord](../core-data-structures/session-query.md) · [SessionEventResultFilter](../core-data-structures/session-query.md) · [SessionEventSearchDocument](../core-data-structures/session-query.md) · [SessionEventSearchPage](../core-data-structures/session-query.md) · [SessionEventSearchRequest](../core-data-structures/session-query.md) · [SessionEventTraceObservation](../core-data-structures/session-query.md) · [SessionEventTraceRequest](../core-data-structures/session-query.md) · [SessionEventWindow](../core-data-structures/session-query.md) · [SessionId](../core-data-structures/core.md) · [SessionLineageTrace](../core-data-structures/session-query.md) · [SessionLogSnapshot](../core-data-structures/session-query.md) · [SessionRecord](../core-data-structures/session-query.md) · [SessionResultFilter](../core-data-structures/session-query.md) · [SessionSearchExecContext](../core-data-structures/session-query.md) · [SessionSearchHit](../core-data-structures/session-query.md) · [SessionSearchPage](../core-data-structures/session-query.md) · [SessionSearchRequest](../core-data-structures/session-query.md) · [SessionSurfaceSnapshot](../core-data-structures/session-query.md) · [SessionTitleObservation](../core-data-structures/session-query.md) · [SessionTitleObservationResult](../core-data-structures/session-query.md) · [SessionTitleSnapshot](../core-data-structures/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](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [PreparedReferencedMessage](../core-data-structures/session-reference.md) · [SessionReferenceCandidate](../core-data-structures/session-reference.md) · [SessionReferenceInput](../core-data-structures/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](../core-data-structures/persistence.md) · [PrepareSessionOptions](../core-data-structures/persistence.md) · [Session](../core-data-structures/session.md) · [SessionId](../core-data-structures/core.md) - -Source: [`packages/core/session/src/index.ts:810`](../../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](../core-data-structures/session.md) · [SessionTitleProvider](../core-data-structures/session-title.md) · [SessionTitleSnapshot](../core-data-structures/session-title.md) - -Source: [`packages/session-title/session-title/src/index.ts:261`](../../packages/session-title/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](../core-data-structures/settings.md) · [SettingsDescriptor](../core-data-structures/settings.md) · [SettingsNamespace](../core-data-structures/settings.md) · [SettingsPathOp](../core-data-structures/settings.md) · [SettingsRegisterOptions](../core-data-structures/settings.md) · [SettingsScope](../core-data-structures/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](../core-data-structures/skills.md) · [SkillDefinition](../core-data-structures/skills.md) · [SkillLookupOptions](../core-data-structures/skills.md) · [SkillProvider](../core-data-structures/skills.md) · [SkillProviderControl](../core-data-structures/skills.md) · [SkillRegistration](../core-data-structures/skills.md) · [SkillSummary](../core-data-structures/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](../core-data-structures/spill.md) · [SpillRef](../core-data-structures/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] -``` - -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) - -## `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-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [ContinuableSetupContribution](../core-data-structures/subagent.md) · [ContinuableStart](../core-data-structures/subagent.md) · [ContinuableStartSpec](../core-data-structures/subagent.md) · [MessageId](../core-data-structures/core.md) · [SessionId](../core-data-structures/core.md) · [SubagentDescendantListEntry](../core-data-structures/subagent.md) · [SubagentFollowupOptions](../core-data-structures/subagent.md) · [SubagentInterruptAuthority](../core-data-structures/subagent.md) · [SubagentListEntry](../core-data-structures/subagent.md) · [SubagentProvider](../core-data-structures/subagent.md) · [SubagentReportOptions](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/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](../core-data-structures/subprocess.md) · [SubprocessSpawnSpec](../core-data-structures/subprocess.md) · [SubprocessTerminalHandle](../core-data-structures/subprocess.md) · [SubprocessTerminalSpawnSpec](../core-data-structures/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](../core-data-structures/system-prompt.md) · [PromptContext](../core-data-structures/system-prompt.md) · [PromptSection](../core-data-structures/system-prompt.md) · [ToolProviderResult](../core-data-structures/system-prompt.md) - -Source: [`packages/core/system-prompt/src/index.ts:325`](../../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](../core-data-structures/core.md) · [TaskDoneListener](../core-data-structures/tasks.md) · [TaskId](../core-data-structures/tasks.md) · [TaskRead](../core-data-structures/tasks.md) · [TaskSnapshot](../core-data-structures/tasks.md) · [TaskStart](../core-data-structures/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 -``` - -Source: [`packages/telemetry/session-telemetry/src/index.ts:140`](../../packages/telemetry/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](../core-data-structures/session.md) · [Message](../core-data-structures/core.md) · [Session](../core-data-structures/session.md) · [TokenMeasurement](../core-data-structures/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](../core-data-structures/core.md) · [PruneResult](../core-data-structures/compaction.md) · [Session](../core-data-structures/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](../core-data-structures/scope.md) · [ToolDefinition](../core-data-structures/tools.md) · [ToolExecutionInput](../core-data-structures/tools.md) · [ToolExecutionMode](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolGuard](../core-data-structures/tools.md) · [ToolRestriction](../core-data-structures/tools.md) · [ToolSchema](../core-data-structures/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](../core-data-structures/user-interaction.md) · [AskUserQuestionRequest](../core-data-structures/user-interaction.md) · [UserInteractionProvider](../core-data-structures/user-interaction.md) - -Source: [`packages/ui/user-interaction/src/index.ts:51`](../../packages/ui/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](../core-data-structures/web.md) · [WebFetchRequest](../core-data-structures/web.md) · [WebFetchResult](../core-data-structures/web.md) · [WebSearchProvider](../core-data-structures/web.md) · [WebSearchRequest](../core-data-structures/web.md) · [WebSearchResult](../core-data-structures/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](../core-data-structures/workflow.md) · [WorkflowStartRequest](../core-data-structures/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](../core-data-structures/core.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/core-data-structures/approval.md b/docs/core-data-structures/approval.md deleted file mode 100644 index f1889b25e2..0000000000 --- a/docs/core-data-structures/approval.md +++ /dev/null @@ -1,89 +0,0 @@ -# User Approval - -English | [中文](approval.zh.md) - -The user-approval seam of [dsh-user-approval](../../packages/ui/user-approval) answers one question: may this specific action proceed? It owns the shared request/outcome vocabulary, the `ctx.approval` dispatch service, the `approval/request` answerer waterfall, the log-only audit pair, and the per-session `ask`/`never` policy. UI channels may provide human answerers; the [ACP automation bridge](../../packages/acp/acp) provides one-shot machine decisions for its own agents. Callers such as [dsh-tools](../../packages/core/tools) and [dsh-tool-bash](../../packages/bash/tool-bash) consume the closed outcome and fail closed unless it is `allowed-once`. - -Source: [`packages/ui/user-approval/src/index.ts`](../../packages/ui/user-approval/src/index.ts) - -## Identity and outcome - -Every request receives a fresh `ApprovalRequestId`. The brand pairs the `approval/asked` and `approval/decided` audit events without making approval ids interchangeable with tool-call or agent/session ids. - -```ts type-equiv -/** - * Pairs one `approval/asked` audit event with its `approval/decided`. - * Service-issued (one fresh id per {@link ApprovalService.request} call). - */ -type ApprovalRequestId = Branded<'ApprovalRequestId'> -``` - -`ApprovalOutcome` is closed and fail-closed. `allowed-once` grants only the asked-about action; callers deny on `rejected`, `cancelled`, and `unavailable`. A missing, non-owning, throwing, or non-conforming answerer becomes `unavailable` rather than opening the gate. - -```ts type-equiv -/** - * Closed approval outcomes: a one-shot grant, explicit rejection, withdrawn - * request, or unavailable answerer. Callers fail closed on `unavailable`. - */ -type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable' -``` - -## Per-session policy - -`ApprovalPolicy` determines what happens before interactive answerers run. `ask` delegates to the composed answerer chain, whose no-answer default is `unavailable`; `never` deterministically returns `rejected` without dispatching any answerer. The effective value is the last `approval/policy` event in the session log, falling back to the service config. `setApprovalPolicy(session, policy)` is the single write path, so replay reconstructs the override. - -```ts type-equiv -/** - * A session's approval policy — what happens to an {@link ApprovalService} - * ask BEFORE any interactive answerer sees it: - * - * - `'ask'` (the default) — delegate to the composed answerers; with none - * composed the chain falls through to the fail-closed `'unavailable'` - * (exactly today's behavior). - * - `'never'` — never prompt anyone: every ask resolves `'rejected'` - * deterministically. The strict headless stance (CI, unattended runs) and - * the policy whose outcome is knowable without asking. - */ -type ApprovalPolicy = 'ask' | 'never' -``` - -Both policies contribute their complete current meaning to the cache-safe runtime-context snapshot. The sourced `user/message` is the durable model-visible input; changing approval state appends a new full snapshot after retained history without rewriting the request header's system prompt. - -## Approval request - -`ApprovalRequest` identifies the agent and tool action closely enough to route and audit the question. It deliberately omits tool arguments: an answerer attaches the prompt to the already-streamed tool call through `callId` instead of rendering a second copy that could drift. - -```ts type-equiv -/** - * Readonly same-process permission question. `callId` links to an already - * presented tool call, so arguments are not duplicated here. - */ -interface ApprovalRequest { - /** - * The agent on whose behalf the question is asked. Routes the question (a - * UI answerer only answers for agents it owns) and receives the audit - * events on its session log. - */ - readonly agent: Agent - /** The tool the question is about (presentation and audit). */ - readonly toolName: string - /** - * The exact tool call being decided, when the asker has one — lets a UI - * attach the prompt to the tool call it already streamed. - */ - readonly callId?: CallId - /** The asker's human-readable explanation of WHY it is asking. */ - readonly reason?: string - /** - * Aborting withdraws the question: the request settles `'cancelled'` - * immediately and a late answer from a still-pending answerer is discarded. - */ - readonly signal?: AbortSignal -} -``` - -## Dispatch and audit - -`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. diff --git a/docs/core-data-structures/approval.zh.md b/docs/core-data-structures/approval.zh.md deleted file mode 100644 index 4822299131..0000000000 --- a/docs/core-data-structures/approval.zh.md +++ /dev/null @@ -1,89 +0,0 @@ -# 用户审批 - -[English](approval.md) | 中文 - -[dsh-user-approval](../../packages/ui/user-approval) 的用户审批 seam 回答一个问题:这个具体操作是否可以继续?它拥有共享的请求/结果词汇、`ctx.approval` 分发服务、`approval/request` 应答者 waterfall(瀑布式事件)、仅记录日志的审计事件对,以及按会话的 `ask`/`never` 策略。UI 通道可以提供人类应答者;[ACP(Agent Client Protocol)自动化桥接层](../../packages/acp/acp)为其拥有的 agent(智能体)提供一次性机器决策。调用方如 [dsh-tools](../../packages/core/tools) 和 [dsh-tool-bash](../../packages/bash/tool-bash) 消费闭合的结果,除非结果为 `allowed-once`,否则一律拒绝。 - -源码:[`packages/ui/user-approval/src/index.ts`](../../packages/ui/user-approval/src/index.ts) - -## 标识与结果 - -每个请求都会获得一个全新的 `ApprovalRequestId`。该品牌类型将 `approval/asked` 与 `approval/decided` 审计事件配对,同时不会让审批 id 与工具调用 id 或 agent/会话 id 互换。 - -```ts type-equiv -/** - * Pairs one `approval/asked` audit event with its `approval/decided`. - * Service-issued (one fresh id per {@link ApprovalService.request} call). - */ -type ApprovalRequestId = Branded<'ApprovalRequestId'> -``` - -`ApprovalOutcome` 是闭合的,且失败时拒绝。`allowed-once` 仅授权所询问的那一个操作;调用方对 `rejected`、`cancelled` 和 `unavailable` 均执行拒绝。缺失、不负责该请求、抛异常或不合规的应答者会产生 `unavailable`,而非放行。 - -```ts type-equiv -/** - * Closed approval outcomes: a one-shot grant, explicit rejection, withdrawn - * request, or unavailable answerer. Callers fail closed on `unavailable`. - */ -type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable' -``` - -## 按会话策略 - -`ApprovalPolicy` 决定在交互式应答者运行之前发生什么。`ask` 委托给组合的应答者链,链的无应答默认值为 `unavailable`;`never` 确定性地返回 `rejected`,不分发任何应答者。生效值为会话日志中最后一条 `approval/policy` 事件,回退到服务配置。`setApprovalPolicy(session, policy)` 是唯一的写入路径,因此回放能重建覆盖值。 - -```ts type-equiv -/** - * A session's approval policy — what happens to an {@link ApprovalService} - * ask BEFORE any interactive answerer sees it: - * - * - `'ask'` (the default) — delegate to the composed answerers; with none - * composed the chain falls through to the fail-closed `'unavailable'` - * (exactly today's behavior). - * - `'never'` — never prompt anyone: every ask resolves `'rejected'` - * deterministically. The strict headless stance (CI, unattended runs) and - * the policy whose outcome is knowable without asking. - */ -type ApprovalPolicy = 'ask' | 'never' -``` - -两种策略都会将各自完整的当前含义贡献给缓存安全的运行时上下文快照。带来源的 `user/message` 是持久化且模型可见的输入;批准状态变化时,会在保留的历史后追加一份新的完整快照,而不改写请求头中的系统提示词。 - -## 审批请求 - -`ApprovalRequest` 以足够精确的方式标识 agent 和工具操作,以便路由和审计该问题。它有意省略工具参数:应答者通过 `callId` 将提示附加到已流式输出的工具调用上,而非渲染一份可能漂移的副本。 - -```ts type-equiv -/** - * Readonly same-process permission question. `callId` links to an already - * presented tool call, so arguments are not duplicated here. - */ -interface ApprovalRequest { - /** - * The agent on whose behalf the question is asked. Routes the question (a - * UI answerer only answers for agents it owns) and receives the audit - * events on its session log. - */ - readonly agent: Agent - /** The tool the question is about (presentation and audit). */ - readonly toolName: string - /** - * The exact tool call being decided, when the asker has one — lets a UI - * attach the prompt to the tool call it already streamed. - */ - readonly callId?: CallId - /** The asker's human-readable explanation of WHY it is asking. */ - readonly reason?: string - /** - * Aborting withdraws the question: the request settles `'cancelled'` - * immediately and a late answer from a still-pending answerer is discarded. - */ - readonly signal?: AbortSignal -} -``` - -## 分发与审计 - -`ctx.approval.request(req)` 要求发起请求的会话处于一个尚未结束的轮次内。它追加 `approval/asked`,获取一个结果,追加对应的 `approval/decided`,然后以该结果完成。`never` 策略在服务内部、waterfall 分发之前强制执行,因此即使后来以 `prepend` 注册的应答者也无法绕过它。应答者在拥有该请求时返回结果,否则调用 `next()` 委托;第一个应答占据唯一的决策槽位。 - -审计事件仅写入日志,不进入模型 transcript(文本记录)。模型可见的行为是调用方派生的工具结果与当前运行时上下文快照。服务 dispose(资源释放)时会移除其上下文贡献;应答者监听器独立地通过 effect 绑定到其所属插件。 diff --git a/docs/core-data-structures/commands.md b/docs/core-data-structures/commands.md deleted file mode 100644 index 1390f736ff..0000000000 --- a/docs/core-data-structures/commands.md +++ /dev/null @@ -1,99 +0,0 @@ -# Human Commands - -English | [中文](commands.zh.md) - -The human-command seam of [`dsh-commands`](../../packages/ui/commands). Interactive adapters use it to discover and directly execute plugin-owned commands for an exact agent without creating a model message. The [command Agent Note](../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md) owns dispatch and lifecycle rationale; the [package README](../../packages/ui/commands/README.md) owns composition and limitations. - -Source: [`packages/ui/commands/src/index.ts`](../../packages/ui/commands/src/index.ts) - -## Input metadata - -The seam exposes one optional unstructured-input hint. Command availability follows plugin composition: every adapter consuming the registry sees every effective definition. - -```ts type-equiv -/** Immutable metadata for a command's optional unstructured input. */ -interface CommandInputDescriptor { - /** Placeholder shown before the user supplies free-form input. */ - readonly hint: string -} -``` - -## Definition - -`CommandDefinition` is the plugin-authored registration. The registry validates and freezes a detached effective definition. - -```ts type-equiv -/** Plugin-owned command registration. */ -interface CommandDefinition { - /** Lowercase command name without the leading slash. */ - readonly name: string - /** Human-readable summary used in discovery UI. */ - readonly description: string - /** Optional free-form input hint advertised to capable clients. */ - readonly input?: CommandInputDescriptor - /** - * Whether `command/run` records `rawInput`. Defaults to true. A command - * whose domain event owns the payload sets this false to avoid duplicating - * that payload in the session log. - */ - readonly recordInput?: boolean - /** Execute against the receiving agent without sending the command to the model. */ - readonly handler: (invocation: CommandInvocation) => CommandResult | Promise -} -``` - -## Invocation and result - -The adapter owns cancellation and passes the exact target agent. `rawInput` begins immediately after the parsed name and retains the adapter-delivered separator and suffix. Results are direct UI outcomes, not tool results or session events. - -```ts type-equiv -/** Invocation passed to one registered command handler. */ -interface CommandInvocation { - /** Exact agent whose human-facing surface received the command. */ - readonly agent: Agent - /** Exact text following the registered command name, including separator whitespace. */ - readonly rawInput: string - /** Cancellation signal owned by the dispatching UI request. */ - readonly signal: AbortSignal -} -``` - -```ts type-equiv -/** Expected command outcome rendered directly by the dispatching UI. */ -type CommandResult = - | { - readonly kind: 'success' - readonly text?: string - /** Earlier authoritative domain event that owns a richer presentation. */ - readonly sourceEventSeq?: number - } - | { readonly kind: 'error'; readonly text: string } -``` - -`sourceEventSeq` is optional and success-only. When present, it names an earlier non-command event in the receiving session log; `command/done` persists the same reference so a client can combine the command lifecycle with that domain projection without parsing `text` or relying on adjacent rows. - -## Discovery and parsing views - -Adapters receive handler-free immutable descriptors after scope resolution. `parseCommand()` returns `ParsedCommand` before registry resolution; syntax-valid input can still name an unavailable command. - -```ts type-equiv -/** Handler-free immutable command view returned to UI adapters. */ -interface CommandDescriptor { - /** Lowercase command name without the leading slash. */ - readonly name: string - /** Human-readable summary used in discovery UI. */ - readonly description: string - /** Optional free-form input hint advertised to capable clients. */ - readonly input?: CommandInputDescriptor -} -``` - -```ts type-equiv -/** Syntactically valid slash command before registry resolution. */ -interface ParsedCommand { - /** Lowercase command name without the leading slash. */ - readonly name: string - /** Exact text following the command name. */ - readonly rawInput: string -} -``` diff --git a/docs/core-data-structures/commands.zh.md b/docs/core-data-structures/commands.zh.md deleted file mode 100644 index 53aec9bd13..0000000000 --- a/docs/core-data-structures/commands.zh.md +++ /dev/null @@ -1,99 +0,0 @@ -# 用户命令 - -[English](commands.md) | 中文 - -[`dsh-commands`](../../packages/ui/commands) 的用户命令 seam。交互式适配器用它发现插件拥有的命令,并针对确切的 agent(智能体)直接执行这些命令,而不创建模型消息。[命令 Agent Note](../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md) 负责分发与生命周期的决策依据;[包 README](../../packages/ui/commands/README.md) 负责组合方式与限制。 - -来源:[`packages/ui/commands/src/index.ts`](../../packages/ui/commands/src/index.ts) - -## 输入元数据 - -该 seam 公开一个可选的非结构化输入提示。命令的可用性由插件组合决定:每个消费注册表的适配器都会看到全部生效定义。 - -```ts type-equiv -/** Immutable metadata for a command's optional unstructured input. */ -interface CommandInputDescriptor { - /** Placeholder shown before the user supplies free-form input. */ - readonly hint: string -} -``` - -## 定义 - -`CommandDefinition` 是由插件编写的注册定义。注册表会验证并冻结一份与原始注册对象脱离的生效定义。 - -```ts type-equiv -/** Plugin-owned command registration. */ -interface CommandDefinition { - /** Lowercase command name without the leading slash. */ - readonly name: string - /** Human-readable summary used in discovery UI. */ - readonly description: string - /** Optional free-form input hint advertised to capable clients. */ - readonly input?: CommandInputDescriptor - /** - * Whether `command/run` records `rawInput`. Defaults to true. A command - * whose domain event owns the payload sets this false to avoid duplicating - * that payload in the session log. - */ - readonly recordInput?: boolean - /** Execute against the receiving agent without sending the command to the model. */ - readonly handler: (invocation: CommandInvocation) => CommandResult | Promise -} -``` - -## 调用与结果 - -取消由适配器负责,适配器会传入确切的目标 agent。`rawInput` 紧接在解析后的名称之后,并保留适配器传入的分隔符与后缀。结果会直接呈现给 UI,而不是工具结果或会话事件。 - -```ts type-equiv -/** Invocation passed to one registered command handler. */ -interface CommandInvocation { - /** Exact agent whose human-facing surface received the command. */ - readonly agent: Agent - /** Exact text following the registered command name, including separator whitespace. */ - readonly rawInput: string - /** Cancellation signal owned by the dispatching UI request. */ - readonly signal: AbortSignal -} -``` - -```ts type-equiv -/** Expected command outcome rendered directly by the dispatching UI. */ -type CommandResult = - | { - readonly kind: 'success' - readonly text?: string - /** Earlier authoritative domain event that owns a richer presentation. */ - readonly sourceEventSeq?: number - } - | { readonly kind: 'error'; readonly text: string } -``` - -`sourceEventSeq` 是可选字段,且只用于成功结果。存在时,它指向接收会话日志中更早的一条非命令事件;`command/done` 会持久化同一引用,让客户端能够将命令生命周期与该领域投影合并,而无须解析 `text` 或依赖相邻行。 - -## 发现与解析视图 - -作用域解析后,适配器会获得不含处理器的不可变描述符。`parseCommand()` 在注册表解析前返回 `ParsedCommand`;语法有效的输入仍可能指向不可用的命令。 - -```ts type-equiv -/** Handler-free immutable command view returned to UI adapters. */ -interface CommandDescriptor { - /** Lowercase command name without the leading slash. */ - readonly name: string - /** Human-readable summary used in discovery UI. */ - readonly description: string - /** Optional free-form input hint advertised to capable clients. */ - readonly input?: CommandInputDescriptor -} -``` - -```ts type-equiv -/** Syntactically valid slash command before registry resolution. */ -interface ParsedCommand { - /** Lowercase command name without the leading slash. */ - readonly name: string - /** Exact text following the command name. */ - readonly rawInput: string -} -``` diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md deleted file mode 100644 index 8f413a7a06..0000000000 --- a/docs/core-data-structures/core.md +++ /dev/null @@ -1,774 +0,0 @@ -# Core Data Structures - -English | [中文](core.zh.md) - -This folder catalogs the **data structures** of the DeepSeek Harness — what each core type represents, its literal shape, and where the full detail lives. It complements [architecture.md](../architecture.md), which describes *behavior* (the service map, the session/turn/step lifecycle, the event taxonomy); this page describes the *vocabulary* that behavior moves around. - -## What counts as "core" - -The harness is a microkernel: a tiny core plus many plugins. Most types belong to one plugin or one capability. A handful, though, are the **spine** — the language the agent loop and its events traffic in on *every* turn, no matter which optional plugins are loaded. Those are "core". - -Precisely, a data structure is **core** if either: - -1. it flows through the agent-loop spine — the loop holds it, derives it, streams it, or logs it on every turn (a `Message`, a `StreamChunk`, a `SessionEvent`, the `Agent` handle itself), independent of which plugins are present; **or** -2. it is the single headline type a plugin author writes against a pipeline — `ToolDefinition` (what every tool *is*). - -Everything else is documented on a **sub-page**, not here. The rule that draws the line: *the type you write, hold, or receive is core; the machinery that types it, renders it, or persists it is a sub-page detail.* So `ToolDefinition` is core, but the `ValueSchemaSpec`/`ParameterSchemaSpec` inference machinery that types it, the `ToolCallView`/`ToolResultView` render-intent vocabulary that renders it, and the `SessionPersistence` seam that stores the event log are not — they live on the sub-pages below. - -| Sub-page | Owns | -|---|---| -| [llm-streaming.md](llm-streaming.md) | the `StreamChunk` wire protocol + adapter contract, `BlockAssembler`, the `LlmAdapter` seam | -| [token-meter.md](token-meter.md) | immutable scalar and positional replay measurements with consumed-log revisions | -| [scope.md](scope.md) | scoped registration identity, dispatch carriers, and the owned `Scope` context | -| [typert.md](typert.md) | Remote invocation descriptors, lookup/Context declarations, TypeRT registries, and the Host Gateway/Client API seams | -| [goal.md](goal.md) | persisted goal identity, lifecycle snapshots, activation, change records, and round attribution | -| [commands.md](commands.md) | the human-command seam: definitions, adapter discovery, direct invocation, results, and parsing views | -| [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnTrigger`/`TurnEndReason`, `deriveMessages()`, execution enclosure, and standalone events | -| [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, JSONL + SQLite backends, `session/flush`, crash recovery, `SessionHeader` | -| [settings.md](settings.md) | the user-settings seam: `SettingsNamespace` registration, layered resolution (defaults → composition `base` → user document), owner scopes, hot commits | -| [credentials.md](credentials.md) | the credential seam: `CredentialRef` references (never values) in configuration, per-operation resolution, UI-safe `CredentialInfo`, provider source layers | -| [session-query.md](session-query.md) | logical records, bounded exact-event reads, relationship traces, semantic filters/documents, and full-text result pages | -| [session-title.md](session-title.md) | durable title snapshots, source provenance, and the asynchronous provider contract | -| [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, prompt sections, and cooperative assembly | -| [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, and the guarded execution pipeline | -| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy | -| [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit and answerer contracts | -| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashProcess` handles | -| [subprocess.md](subprocess.md) | the subprocess seam: fully-explicit `SubprocessSpawnSpec`, offset-based output readers, unclassified `SubprocessOutcome`, and the managed `DSH_*` environment vocabulary | -| [pty.md](pty.md) | persistent terminal ids, backend/session contracts, send readiness, bounded reads, and owner-visible snapshots | -| [sandbox.md](sandbox.md) | per-session policy resolution and the process-confinement seam: file-effect modes, execution/provider policies, `ConfinedArgv`, enforcement and fail-closed errors | -| [code-runtime.md](code-runtime.md) | the code-execution seam: `CodeRunRequest`/`Result`, binding namespaces, captured logs, the `CodeRunFailure` taxonomy | -| [filesystem.md](filesystem.md) | the filesystem seam: `FsTarget`, read/write/edit outcomes, observed-file state, `FsErrorCode` | -| [lsp.md](lsp.md) | the LSP navigation seam: `LspQueryRequest`/`Result`, `LspProvider`/`Service`, four operations, `LspError` | -| [skills.md](skills.md) | the skill service: discovery priority, `SkillSummary`/`SkillDefinition`, session-prefix catalog, model-facing `skill` loading | -| [compaction.md](compaction.md) | the compaction seam: the `compact/*` session events, `CompactionResult`, the `CompactService` interface | -| [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split | -| [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` | -| [spill.md](spill.md) | the spill storage seam: `SaveTextSpill`, `SpillOwner`/`SpillSource`, `SpillRef`, the branded `SpillLocator` | -| [workflow.md](workflow.md) | the workflow seam: `WorkflowStartRequest`, `WorkflowMeta`, `WorkflowRun`/`Result`, the `workflow/*` event payloads, `WorkflowError` fatality | - -> 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). - -## The `…Map → derived-union` pattern - -Almost every extensible sum type in the harness follows one shape: an interface keyed by a discriminant tag (the `…Map`), from which the union is derived with `keyof`. Plugins add variants by **declaration merging** — no edit to the owning package. - -```ts ignore-check -// The pattern, schematically: -interface ThingMap { - 'a': { kind: 'a'; /* … */ } - 'b': { kind: 'b'; /* … */ } -} -type ThingKind = keyof ThingMap // 'a' | 'b' -type Thing = ThingMap[keyof ThingMap] // the discriminated union - -// A plugin extends it without touching the source package: -declare module '@deepseek-ai/dsh-llm' { - interface ThingMap { - 'c': { kind: 'c'; /* … */ } - } -} -``` - -Five canonical maps use this pattern; a plugin author extends these: - -| Map | Package | Derives | Catalog | -|---|---|---|---| -| `ContentBlockMap` | dsh-llm | `ContentBlock` | [below](#content-blocks-and-messages) | -| `MessageSourceMap` | dsh-llm | `MessageSource` | [below](#content-blocks-and-messages) | -| `FinishReasonMap` | dsh-llm | `FinishReason` | [below](#the-model-request-and-result) | -| `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.md) | -| `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.md) | - -Two large discriminated unions are the ones consumers `switch` over most: **`StreamChunk`** (the streaming protocol) and **`SessionEvent`** (the log entry). Per the repo convention, `switch` on the tag — don't chain `if`s — so each arm narrows and a typo'd tag fails to compile. - -## Branded IDs - -IDs that cross package boundaries are **branded** — structurally strings, but non-interchangeable at the type level (a `SessionId` cannot be passed where a `CallId` is expected). Construction goes through a per-type factory; comparison, logging, and JSON behave as ordinary strings. - -The `Branded` primitive lives in its own type-only package, [dsh-brand](../../packages/util/brand) (no runtime code, no harness-package dependency), so any package can brand the ids it owns without depending on an unrelated capability package. - -Source: [`packages/util/brand/src/index.ts`](../../packages/util/brand/src/index.ts) - -```ts type-equiv -/** A string carrying a compile-time-only brand `B`. */ -type Branded = string & { readonly [BRAND]: B } -``` - -The two core IDs are `CallId` (correlates a tool call with its result; dsh-llm) and `SessionId` (the shared live agent and durable session identity; dsh-session). Capability packages brand their own ids too, such as `TaskId` in [tasks.md](tasks.md). - -## Content blocks and messages - -A conversation is `Message`s; a message is an array of typed **content blocks**. The block union derives from `ContentBlockMap`. - -Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) - -```ts type-equiv -/** - * Merge-extensible content blocks keyed by `type`. New core blocks must land - * with adapter, UI, and compaction support. - */ -interface ContentBlockMap { - 'text': TextBlock - 'reasoning': ReasoningBlock - 'tool-call': ToolCallBlock - 'tool-result': ToolResultBlock -} -``` - -The block interfaces (full fields in source): `TextBlock` (`text`), `ReasoningBlock` (thinking, distinct from visible text), `ToolCallBlock` (`id: CallId`, `name`, raw-JSON `arguments`), `ToolResultBlock` (`toolCallId`, nested `content: ContentBlock[]`, `isError?`). `ContentBlock = ContentBlockMap[ContentBlockType]`. The core set is limited to blocks every shipping path honors — multimodal content (images, audio, …) has no core block type; a feature that needs one adds it via the merge-extensible map together with the adapter/UI/compaction support that honors it. - -Source: [`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts) - -A `Message` is one identified, immutable role/source/content value. Model-produced assistant messages carry provider/model ownership and optional adapter-private replay metadata in their source: - -```ts type-equiv -/** Provider ownership and adapter-private replay data for an assistant message. */ -interface AssistantProvenance { - /** Provider route that produced the message. */ - provider: string - /** Provider model id that produced the message. */ - model: string - /** - * Lossless-JSON adapter state needed to replay the provider response. - * `LlmService` exposes it to a target adapter only when that adapter instance - * currently owns both this historical provider and the target provider. - */ - replayState?: unknown -} -``` - -```ts type-equiv -/** One immutable message representation shared by delivery, durable history, and model requests. */ -interface Message { - /** Stable identity preserved across every representation boundary. */ - readonly id: MessageId - /** Provider-neutral conversation role. */ - readonly role: 'system' | 'user' | 'assistant' - /** Exact model-facing blocks. */ - readonly content: ContentBlock[] - /** Required producer provenance. */ - readonly source: MessageSource -} -``` - -Where a message came from is itself a merge-extensible sum type: - -```ts type-equiv -/** - * Where a message (or injected content) came from. - * Merge-extensible sum type — plugins add their own `kind`s. - */ -interface MessageSourceMap { - user: { kind: 'user' } - plugin: { kind: 'plugin'; plugin: string } & ContextFormed - model: ModelMessageSource - tool: ToolMessageSource -} -``` - -Provenance and shape are two independent axes. `kind` answers *who produced this*; the optional `form` a producer mixes in answers *what shape of information it is*, so several producers may share one presentation and one producer may emit more than one shape over a session. The vocabulary is semantic and grows one value at a time; an absent or unrecognized value is the documented default, presented as opaque content: - -```ts type-equiv -/** - * What SHAPE of information a producer-supplied context carries, declared by - * the producer beside its provenance. - * - * `MessageSource.kind` answers *who produced this*; `form` answers *what kind - * of thing it is*, and the two axes are deliberately independent — several - * producers share one form (three snapshot producers today), and one producer - * may emit more than one form over a session. - * - * The vocabulary is SEMANTIC, never visual: a value states that the content is - * a file's instructions or a catalog of available items, and a consumer decides - * what that looks like. Colors, icons, ordering, and collapse defaults are the - * consumer's business and must not enter this union. It grows one value at a - * time as producers gain the structured fields their form needs; an absent or - * unknown value is the documented default, presented as opaque content. - */ -type ContextForm = - /** Instructions read out of workspace files the model is expected to follow. */ - | 'instructions' - /** A catalog of items available in this session, republished as it changes. */ - | 'catalog' - /** Current state, where a later snapshot from the same producer supersedes an earlier one. */ - | 'snapshot' - /** A one-off account of something that just happened; it supersedes nothing. */ - | 'notice' - /** A message another agent addressed to this one. */ - | 'relay' - /** Material lifted out of another session's log, possibly reduced on the way in. */ - | 'recall' -``` - -```ts type-equiv -/** One named contribution to a `snapshot`-form context, in assembly order. */ -interface ContextSnapshotSection { - /** The contributing subsystem's name. */ - readonly name: string - /** That contribution's model-facing text, exactly as assembled. */ - readonly text: string -} -``` - -```ts type-equiv -/** - * Producer-declared {@link ContextForm} and the fields that form requires, - * mixed into the source shapes that carry one. - * - * Discriminated by `form` so a producer cannot declare a shape without the - * facts that shape is presented from: a `notice` must record its one-line - * account, a `snapshot` its sections. Omitting `form` stays valid — an - * undeclared context is the documented default. - */ -type ContextFormed = - | { readonly form?: never } - | { readonly form: 'instructions' } - | { readonly form: 'catalog' } - | { - readonly form: 'snapshot' - /** The named contributions this snapshot assembled, in order. */ - readonly sections: readonly ContextSnapshotSection[] - } - | { - readonly form: 'notice' - /** One-line account of what happened, shown without expanding the row. */ - readonly summary: string - } - | { readonly form: 'relay' } - | { readonly form: 'recall' } -``` - -## Streaming - -Adapters emit a raw **chunk** protocol; the loop logs the chunks (replay fidelity) while feeding the same chunks through a `BlockAssembler` to rebuild blocks and messages. `StreamChunk` is a closed discriminated union over `type` — `block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`. - -The full union, the adapter contract (usage-before-finish, raw-JSON tool arguments, the two sanctioned error paths), and `BlockAssembler` live on **[llm-streaming.md](llm-streaming.md)**. - - - -## The model request - -One model call is a fully-assembled `GenerateOptions`. The adapter answers with a raw `StreamChunk` stream; the consumer assembles it with `BlockAssembler` (see [llm-streaming.md](llm-streaming.md)). - -Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) - -Provider and model discovery uses small provider-neutral descriptors. A model catalog is advisory: routing still keys on a registered provider, and an adapter may accept unlisted model ids. - -Registering an adapter returns a handle: the disposer, plus the atomic route replacement a plugin whose route set is user-configurable needs. - -```ts type-equiv -/** - * What {@link LlmService.registerAdapter} returns: the disposer, plus an - * atomic route replacement for the same adapter instance. - */ -interface AdapterRegistrationHandle { - /** Release every route this registration currently holds. */ - (): void - /** - * Replace this registration's routes with `providers`, keeping the same - * adapter instance. The candidate set is validated in full first — a - * conflict with another adapter, an invalid name, or bad provider metadata - * throws and leaves the current routes untouched — and the swap itself is - * one synchronous section, so no request can observe a gap. An empty array - * is legal here (a settings section that emptied holds zero routes while - * staying registered), unlike an empty initial registration. - * - * Throws `LlmError` with code `REGISTRATION_DISPOSED` once the registration - * has been released: its routes are gone and its disposer has already run, - * so anything registered afterwards would have no owner left to release it. - * @param providers - the complete next route set for this registration. - */ - replace(providers: string[]): void -} -``` - -```ts type-equiv -/** Display metadata for one registered provider route. */ -interface LlmProviderInfo { - /** Provider route key used by {@link GenerateOptions.provider}. */ - id: string - /** Human-readable provider name for selectors and diagnostics. */ - name: string -} -``` - -Adapter plugins additionally declare which routes *could* run through `registerConfigurableProviders()`, addressing each one's user-settings section, so configuration surfaces can offer dormant providers before any route registers. - -```ts type-equiv -/** - * One provider route an adapter plugin can activate through configuration, - * whether or not the route is currently registered. Configuration surfaces - * merge this directory with `listProviders()` to offer every configurable - * provider alongside its live/dormant state. - */ -interface LlmConfigurableProvider { - /** Provider route key this entry activates when configured. */ - provider: string - /** Human-readable provider name for configuration surfaces. */ - displayName: string - /** User-settings namespace whose section configures this provider. */ - settingsNs: string - /** - * Path from that namespace's section root to this provider's profile - * object; empty when the whole section is the profile. - */ - settingsPath: readonly string[] - /** - * Whether the owning adapter knows this route only because configuration - * declared it — a gateway or self-hosted server it ships nothing about. - * Absent means the adapter draws no such distinction; false means it does - * and this route is one of its own. Only the adapter can answer: a stored - * profile is how a user-added route AND a corrected shipped one both look - * from outside. - */ - declared?: boolean -} -``` - -```ts type-equiv -/** One adapter-discovered model; catalog membership is advisory, not request validation. */ -interface LlmModelInfo { - /** Provider route that owns this model entry. */ - provider: string - /** Model id passed to {@link GenerateOptions.model}. */ - id: string - /** Human-readable model name for selectors. */ - name: string - /** Optional user-facing distinction from otherwise similar models. */ - description?: string -} -``` - -A provider a surface is still drafting has no route and no catalog, so interrogation is described separately: the request carries the draft the user is editing, and the reply is candidates a surface may adopt rather than a catalog it must serve. - -```ts type-equiv -/** - * One interrogation of a provider endpoint that configuration has not stored - * yet. Configuration surfaces send the draft a user is still editing, so the - * request carries the endpoint and credential directly instead of naming a - * route: a provider being added has no route to name. - */ -interface LlmModelDiscoveryRequest { - /** - * Route the draft is editing, when it edits an existing one. A route whose - * adapter already knows its models answers from that knowledge instead of - * asking the endpoint — the adapter's own registry is the better answer, and - * it costs no network call. - */ - provider?: string - /** - * Endpoint to interrogate. Optional because a route the adapter already - * describes needs none; a route it does not must supply one. - */ - baseURL?: string - /** Wire protocol the endpoint speaks, when the draft names one. */ - api?: string - /** Credential for this interrogation alone; the harness never stores it. */ - apiKey?: string - /** Caller cancellation; implementations must settle promptly after it aborts. */ - signal?: AbortSignal -} -``` - -```ts type-equiv -/** - * One model an endpoint reports about itself. Every field but the id is - * optional because most provider listings disclose an id and nothing else; - * a surface adopting one of these still owes the capacities its adapter needs. - */ -interface LlmDiscoveredModel { - /** Model id the endpoint accepts. */ - id: string - /** Human-readable name when the endpoint supplies one. */ - name?: string - /** Maximum combined request and response context, when disclosed. */ - contextWindow?: number - /** Maximum output tokens, when disclosed. */ - maxTokens?: number -} -``` - -Correctness-sensitive metadata is resolved separately from the advisory catalog and is owned by the adapter serving the exact route. Context capacity, adapter call defaults, and reasoning choices share one exact-model result so consumers do not repeat authoritative model resolution. - -```ts type-equiv -/** Provider-owned context capacity for one exact provider/model route. */ -interface LlmModelContext { - /** Maximum combined request and response context in tokens. */ - contextWindow: number -} -``` - -Reasoning effort is another exact-route capability. The core brands identifiers but does not enumerate their values; each adapter owns the ordered set, display names, and optional deployment default. - -```ts type-equiv -/** Adapter-owned identifier for one model's selectable reasoning effort. */ -type ReasoningEffortId = Branded<'ReasoningEffortId'> -``` - -```ts type-equiv -/** Display metadata for one adapter-owned reasoning effort. */ -interface LlmReasoningEffortInfo { - /** Opaque stable value accepted by {@link GenerateOptions.reasoningEffort}. */ - id: ReasoningEffortId - /** Human-readable effort name for selectors and diagnostics. */ - name: string - /** Optional user-facing distinction from otherwise similar efforts. */ - description?: string -} -``` - -```ts type-equiv -/** Selectable reasoning efforts for one exact provider/model route. */ -interface LlmModelReasoningInfo { - /** Supported efforts in adapter-preferred display order. */ - efforts: readonly LlmReasoningEffortInfo[] - /** - * Adapter-configured default materialized into requests when callers omit - * an effort. Absence preserves the provider's own default. - */ - defaultEffort?: ReasoningEffortId -} -``` - -```ts type-equiv -/** Exact-route model metadata resolved by its owning adapter. */ -interface LlmResolvedModelInfo extends LlmModelInfo { - /** Provider-owned context capacity when known. */ - context?: LlmModelContext - /** Adapter-configured per-request output cap materialized when callers omit one. */ - defaultMaxTokens?: number - /** Adapter-owned selectable reasoning levels when exposed. */ - reasoning?: LlmModelReasoningInfo -} -``` - -```ts type-equiv -/** A single model request, fully assembled. */ -interface GenerateOptions { - /** Registered provider route selecting the adapter instance. */ - provider: string - model: string - /** Adapter-owned reasoning effort selected for this exact model. */ - reasoningEffort?: ReasoningEffortId - /** - * Ordered conversation messages, exactly as the provider sees them (after - * the `system` slot). A loop-built request assembles them as - * the derived history (dsh-agent-loop); a hand-built one-shot passes any list. - */ - messages: Message[] - /** System prompt text (adapters map to the provider's system slot). */ - system?: string - /** Tool schemas (adapters map to the provider's `tools` field). */ - tools?: ToolSchema[] - temperature?: number - maxTokens?: number - /** - * Stop sequences: generation halts as soon as the model produces any one of - * these strings (adapters map to the provider's stop field, e.g. OpenAI - * `stop`). The stop string itself is not included in the output. - */ - stop?: string[] - signal?: AbortSignal - /** - * Session identity stamped by the loop for listener routing. Adapters ignore - * it; replay uses it to keep concurrent parent and child cursors independent. - */ - sessionId?: Branded<'SessionId'> - /** - * Provider-neutral classification for an auxiliary model call. Adapters may - * map the purpose to model-hidden transport metadata or purpose-specific - * generation policy. Ordinary conversation requests leave it unset. - */ - purpose?: 'compaction' | 'session-title' -} -``` - -Why a model response stopped is a merge-extensible reason. Terminal provider failures carry the streaming contract's [`LlmFailure`](llm-streaming.md#llmfailure): - -```ts type-equiv -/** - * Why a model response stopped. - * Merge-extensible so adapters can surface provider-specific reasons. - */ -interface FinishReasonMap { - 'stop': { kind: 'stop' } - 'tool-calls': { kind: 'tool-calls' } - 'max-tokens': { kind: 'max-tokens' } - 'aborted': { kind: 'aborted'; failure: LlmFailure } - 'error': { kind: 'error'; failure: LlmFailure } -} -``` - -`FinishReason = FinishReasonMap[keyof FinishReasonMap]`. `TokenUsage` (per-call accounting with disjoint cache fields) is detailed on [llm-streaming.md](llm-streaming.md). - -`GenerateOptions.tools` carries `ToolSchema` — the JSON-schema description of a tool, as sent to the model. It is declared in dsh-llm (not dsh-tools) precisely because it is part of the request the loop assembles every step: - -```ts type-equiv -/** - * JSON-schema description of a tool, as sent to the model. - * - * Declared here (not in dsh-tools) because it is part of {@link GenerateOptions}; - * dsh-tools' ToolDefinition and dsh-system-prompt's PromptAssembly both import - * it from this package. - */ -interface ToolSchema { - name: string - description: string - /** JSON Schema object for the arguments. */ - parameters: Record -} -``` - -The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition` that produces it (schema + `execute`) is on [tools.md](tools.md). - -### The request envelope: `LlmCallConfig` and the logged header - -The loop builds each request from logged state. `EpochHeader` records call config, adapter-default provenance, rendered prompt, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md). - -`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus provenance under the turn signal. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests. - -On the wire, a loop-built request reads the `system` slot (the rendered prompt assembly) followed by the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The dev invariant recomputes exactly this equation against every loop-built request. - -```ts type-equiv -/** - * Provider, model, reasoning effort, and sampling scalars of one conversation's - * requests. Every field maps 1:1 onto the same-named `GenerateOptions` field; - * the loop builds requests from the logged header rather than accepting these - * per call. - */ -interface LlmCallConfig { - provider: string - model: string - reasoningEffort?: ReasoningEffortId - temperature?: number - maxTokens?: number - stop?: string[] -} -``` - -```ts type-equiv -/** - * Effective config fields supplied by exact-model adapter resolution rather - * than by the caller's request proposal. - */ -interface LlmCallConfigAdapterDefaults { - reasoningEffort?: true - maxTokens?: true -} -``` - -## Sessions - -A `Session` is an **append-only log** of typed `SessionEvent`s — the single source of truth. The LLM message history is *derived* from the log (`deriveMessages()`), not stored separately. The event vocabulary derives from `SessionEventMap`: - -Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts) - -```ts type-equiv -/** - * One immutable entry in the session log. - * - * A proper discriminated union over `type` (not independent `type`/`data` - * unions), so `switch (event.type)` narrows `event.data` without casts. - * - * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional: - * they only exist on {@link SurfaceEventType} variants (`user/message`, - * `assistant/message`, `tool/result`). - * Non-surface events (boundary markers, chunks, usage, errors) never carry - * surface metadata — the compiler enforces this at `Session.append()` - * call sites. - */ -type SessionEvent = { - [K in SessionEventType]: { - type: K - /** Monotonic sequence number within the session. */ - seq: number - /** Unix epoch milliseconds. */ - time: number - data: SessionEventMap[K] - } & (K extends SurfaceEventType ? { - /** - * Seq numbers of events that are provenance sources of this event - * (e.g. the `assistant/chunk` seqs that built an `assistant/message`, - * or the surface nodes shadowed by a compaction replace node). An - * `assistant/message` may carry a present empty array for a known empty - * provider stream; omission means unrecorded provenance. - */ - sourceEventSeqs?: number[] - /** How this event entered the surface; absent for non-surface events. */ - surfaceOp?: SurfaceOp - } : object) -}[T] -``` - -The session event variants, `deriveMessages()` projection rules, `TurnEndReason` vocabulary, and execution-enclosure and standalone-event rules are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` seam, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**. - -## The agent handle - -`Agent` is the surface every plugin (UI, hooks, orchestrators) programs against. The concrete implementation is package-internal to dsh-agent-loop; nothing outside the loop depends on it. - -Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) - -```ts type-equiv -/** One of the two ordered pending-message lists owned by an agent. */ -type InboxTarget = 'next-turn' | 'next-step' -``` - -Every pending occurrence is its `UserMessage`; `MessageId` is the sole identity. `Inbox.append`, `prepend`, `replace`, `remove`, `clear`, `splice`, and `claim` record normalized durable `agent/inbox/spliced` mutations and reject duplicate pending ids. `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists; replacement may change identity and emits the old message as discarded followed by the new message as inserted. Ordinary removals and `clear()` are cancellations. `claim(target)` removes the proposed step batch — all `next-step` input plus, at a turn boundary, one `next-turn` message — through pure deletion splices without emitting discarded notifications, and the loop separately emits per-message claimed notifications. Whole-queue consumers such as UI projections reconstruct `nextTurn` and `nextStep` from the durable splices, while consumers following one message use the exact `agent/inbox/inserted`, `claimed`, and `discarded` notifications. - -```ts type-equiv -/** Options for {@link Agent.cancel}. */ -interface CancelOptions { - /** - * Preserve queued and steering inbox items instead of discarding them. The - * active turn is still aborted, but un-started and pending work survives for a - * later turn and no canceled inbox splice is logged. - */ - keepInbox?: boolean | undefined -} -``` - -```ts type-equiv -/** Why an active agent driver was cancelled. */ -type AgentCancelCause = - | { readonly kind: 'user' } - | { readonly kind: 'parent' } - | { readonly kind: 'hook'; readonly reason: string } - | { readonly kind: 'disposed' } -``` - -`Agent` is an interface over the public live-agent contract. Its unified `send` method exposes target and wakeup routing directly; `followup`, `steer`, and `inject` are fixed-preset aliases. - -```ts type-equiv -/** Public live-agent handle. */ -interface Agent { - /** The single identity shared with {@link session}. */ - readonly id: SessionId - /** The provider route and model this agent's requests use. */ - readonly options: AgentOptions - /** The live session this agent drives; its log is the durable source of truth. */ - readonly session: Session - /** The agent-owned projection of durable pending work. */ - readonly inbox: Inbox - /** The current lifecycle state, mirrored on every `agent/status` transition. */ - readonly status: AgentStatus - /** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */ - readonly ctx: Context - - /** - * Clear queued and steering work — unless `keepInbox` — and abort the active - * turn or between-turn task. The first cause wins for that activity. With no - * active activity, cancellation is a no-op and does not arm later work. - * @param cause - the stable caller intent carried by the active operation signal. - * @param options - cancellation options; `keepInbox` preserves pending work. - */ - cancel(cause: AgentCancelCause, options?: CancelOptions): void - - /** - * Resolve after the current whole-agent activity reaches quiescence. This - * follows replacement work started before the observed driver retires, - * but does not identify the settlement of any particular message. - * @returns fulfillment after no active driver or maintenance task remains. - */ - whenIdle(): Promise - - /** - * Run one non-turn maintenance task from the true idle phase. The task starts - * synchronously after claiming that phase; later waking input remains in the - * inbox until the task settles, while public status stays `idle`. - * `whenIdle()` follows both the task and any waking work released behind it. - * @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}. - * @throws synchronously when turn-driving or another maintenance task already owns the agent. - * @returns the task promise. - */ - runMaintenance(task: (signal: AbortSignal) => Promise): Promise - - /** - * Route identified input to an inbox boundary and optionally wake the driver. - * Waking input submitted after active cancellation is queued for the next - * turn and runs when the aborted activity converges to idle; a `disposed` - * cancel leaves it parked. A wake submitted while already idle always opens - * its turn boundary, even when its message is cleared before the driver - * claims ([cancel-convergence wake latch](../../../../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)). - * @param message - identified content and its producer provenance. - * @param target - the preferred next-turn or next-step inbox boundary. - * @param wakeup - whether delivery may wake the driver. - */ - send(message: UserMessage, target: InboxTarget, wakeup: boolean): void - - /** - * Queue an ordinary follow-up turn and wake the driver. The item becomes the - * sole ordinary message of its own turn. - * @param message - identified prompt content and its producer provenance. - */ - followup(message: UserMessage): void - - /** - * Submit steering for the nearest step. An idle driver starts a turn; - * a running driver consumes it at its next step boundary. - * A rejected step leaves steering parked in the inbox until the next - * wake; cancellation or disposal may discard pending steering. - * @param message - identified steering content and its producer provenance. - */ - steer(message: UserMessage): void - - /** - * Queue model-facing context for the next pre-step without waking the - * driver. A running driver claims it at the nearest later step boundary; - * idle drivers leave it pending until follow-up or steering - * wakes them. It may miss a request whose pre-step already claimed its - * batch. Cancellation or disposal may discard pending context. - * @param message - identified injected context and its producer provenance. - */ - inject(message: UserMessage): void -} -``` - -`AgentStatus` is `'idle' | 'running'`, and `SessionId` is branded. Disposal removes the agent from the registry and emits `agent/disposed`; it is not a terminal status value. `running` describes the driver-wide drain interval and may span consecutive queued turns; it does not prove a turn is still open. `followup()` returns no handle: its `MessageId` identifies durable inbox insertion, claim, and discard facts, not a later assistant output or turn ending. `whenIdle()` observes the whole agent, so callers may call a receipt-to-idle interval a run only when they explicitly own that interval ([decision](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md)). `AgentOptions` is merge-extensible: core declares `provider?`, `model?`, and `maxTokens?` (dispatch requires provider and model after `agent/request`). When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default. - -The cause is a TypeScript-enforced same-process input. An active cancellation holder copies it into the runtime-only `AbortSignal.reason`; a signal grants cooperating listeners no classification authority. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result. - -The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits. - -## Initiating Agent - -The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns its lifetime and boundary rules. - -## Interception decisions - -Pre-step decisions use the same identified `UserMessage` shape as durable user-role input. The entered batch is authoritative and preserves every message's identity and provenance. Hook bridges map their native decision fields onto this typed result. - -Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) - -`agent/pre-step` receives one payload carrying the exclusive claimed batch (`messages`), the proposed step's coordinates (`turn`, `step`), and the current turn's cancellation `signal`. The initial proposal runs inside an open turn before any step; a tool continuation may submit an empty claimed batch between steps: - -It returns a `PreStepDecision`. Reject opens no step. Enter supplies the complete message batch appended after `step/start`; claimed messages omitted by the final decision remain removed, while input inserted after the claim stays pending: - -```ts type-equiv -/** Whether and with which messages the loop enters a proposed step. */ -type PreStepDecision = - | { kind: 'reject' } - | { kind: 'enter'; messages: UserMessage[] } -``` - -`agent/request-error` runs after a failed model step closes and before its turn closes. Listeners can repair durable state or await policy work while the failed turn's signal is still live. A handling listener returns `{ kind: 'retry' }` without calling `next()`; the default `undefined` leaves the failure terminal. - -```ts type-equiv -/** Action returned by a listener that owns model-request recovery. */ -type RequestErrorAction = { kind: 'retry' } | undefined -``` - -`agent/pre-step` is the single serial boundary before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain. - -`agent/session-start` carries a `SessionStartSource` (why the session lifecycle began; a bridge keys its SessionStart matcher on it): - -```ts type-equiv -/** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */ -type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact' -``` - -## `ToolDefinition` - -The one pipeline-authoring type that is core: what every registered tool *is* — a model-facing `ToolSchema` plus an `execute` function and optional final-content and UI callbacks. A tool author rarely constructs it by hand (the `defineTool` DSL builds it with typed args), but it is the contract the registry holds and the loop dispatches through. - -Its full fields, the `defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` typed schema DSL, the `ToolExecution`/`ToolExecutionResult` waterfall shapes, and the tool-presentation UI vocabulary are on **[tools.md](tools.md)**. diff --git a/docs/core-data-structures/core.zh.md b/docs/core-data-structures/core.zh.md deleted file mode 100644 index d0f02f0cfc..0000000000 --- a/docs/core-data-structures/core.zh.md +++ /dev/null @@ -1,782 +0,0 @@ -# 核心数据结构 - -[English](core.md) | 中文 - -本目录编目 DeepSeek Harness 的**数据结构**:每个核心类型代表什么、它的字面形状,以及完整细节在哪里。它与 [architecture.md](../architecture.md) 互补——后者描述*行为*(服务映射、会话/轮次/步骤生命周期、事件分类体系);本页描述行为所操作的*词汇*。 - -## 什么算"核心" - -harness 是一个微内核:一个极小的核心加上众多插件。大多数类型属于某一个插件或某一项能力。但有少数类型构成**主干**——agent loop(智能体循环)及其事件在*每一个*轮次中使用的语言,无论加载了哪些可选插件。这些就是"核心"。 - -精确地说,一个数据结构是**核心**的,当且仅当满足以下条件之一: - -1. 它流经 agent loop 主干——循环在每个轮次中持有、派生、流式输出或记录它(`Message`、`StreamChunk`、`SessionEvent`、`Agent` 句柄本身),与当前加载了哪些插件无关;**或者** -2. 它是插件作者面向某条流水线编写的代表性类型——`ToolDefinition`(每个工具*是什么*)。 - -其他一切都记录在**子页面**上,而非本页。划线的规则是:*你编写、持有或接收的类型是核心;为它提供类型推导、渲染或持久化的机制是子页面细节*。因此 `ToolDefinition` 是核心,但为它提供类型推导的 `ValueSchemaSpec`/`ParameterSchemaSpec` 机制、为它提供渲染意图的 `ToolCallView`/`ToolResultView` 词汇,以及存储事件日志的 `SessionPersistence` seam 都不是——它们分别在下列子页面中。 - -| 子页面 | 负责内容 | -|---|---| -| [llm-streaming.md](llm-streaming.md) | `StreamChunk` 协议格式(wire format)+ 适配器契约(adapter contract)、`BlockAssembler`、`LlmAdapter` seam | -| [token-meter.md](token-meter.md) | 不可变的标量与位置回放度量,附带已消费日志修订号 | -| [scope.md](scope.md) | 作用域注册标识、dispatch 载体,以及拥有的 `Scope` 上下文 | -| [typert.md](typert.md) | Remote 调用 descriptor、lookup/Context 声明、TypeRT 注册表,以及 Host Gateway/Client API seam | -| [goal.md](goal.md) | 持久 goal 标识、生命周期快照、激活、变更记录与 Round 归属 | -| [commands.md](commands.md) | 人类命令 seam:定义、适配器发现、直接调用、结果与解析视图 | -| [session.md](session.md) | 完整的 `SessionEventMap` 变体目录、`TurnTrigger`/`TurnEndReason`、`deriveMessages()`、执行封闭与独立事件 | -| [persistence.md](persistence.md) | 持久性 seam:`SessionPersistence`、JSONL + SQLite 后端、`session/flush`、崩溃恢复、`SessionHeader` | -| [settings.md](settings.md) | 用户设置 seam:`SettingsNamespace` 注册、分层解析(默认值 → 组合 `base` → 用户文档)、owner scope、热提交 | -| [credentials.md](credentials.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、provider 来源层 | -| [session-query.md](session-query.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 | -| [session-title.md](session-title.md) | 持久标题快照、来源 provenance 与异步提供方契约 | -| [system-prompt.md](system-prompt.md) | 逐次组装的上下文、工具提供方结果、提示词段落与协作式组装 | -| [tools.md](tools.md) | `ToolDefinition` 完整字段、schema DSL、`ToolExecution`/`ToolResult`、工具展示 UI 类型,以及受保护的执行流水线 | -| [user-interaction.md](user-interaction.md) | UI 支持的人工问答 seam:`AskUserQuestionRequest`、answer/options 词汇、提供方 API、错误分类体系 | -| [approval.md](approval.md) | 一次性用户审批 seam:`ApprovalRequest`、`ApprovalOutcome`、逐会话策略、审计与 answerer 契约 | -| [bash.md](bash.md) | bash 执行器 seam:`BashExecRequest`/`Spec`、`BashRunResult`、后台 `BashProcess` 句柄 | -| [subprocess.md](subprocess.md) | 子进程 seam:完全显式的 `SubprocessSpawnSpec`、基于偏移的输出读取器、不含分类的 `SubprocessOutcome`,以及受管 `DSH_*` 环境词汇 | -| [pty.md](pty.md) | 持久化终端 ID、后端/会话契约、发送就绪状态、有界读取与 owner 可见快照 | -| [sandbox.md](sandbox.md) | 每会话策略解析与进程约束 seam:文件效果模式、执行/提供方策略、`ConfinedArgv`、强制执行与故障关闭错误 | -| [code-runtime.md](code-runtime.md) | 代码执行 seam:`CodeRunRequest`/`Result`、绑定命名空间、捕获日志、`CodeRunFailure` 分类体系 | -| [filesystem.md](filesystem.md) | 文件系统 seam:`FsTarget`、读/写/编辑结果、观测到的文件状态、`FsErrorCode` | -| [lsp.md](lsp.md) | LSP 导航 seam:`LspQueryRequest`/`Result`、`LspProvider`/`Service`、四种操作、`LspError` | -| [skills.md](skills.md) | skill(技能)服务:发现优先级、`SkillSummary`/`SkillDefinition`、会话前缀目录、面向模型的 `skill` 加载 | -| [compaction.md](compaction.md) | 压缩(compaction)seam:`compact/*` 会话事件、`CompactionResult`、`CompactService` 接口 | -| [subagent.md](subagent.md) | subagent seam:命名提供方注册表、`SubagentStartRequest`/`Result`/`Run`、启动时与运行时能力拆分 | -| [web.md](web.md) | Web 访问 seam:`WebSearchRequest`/`Result`、`WebFetchRequest`/`Result`、`WebFetchBody`、提供方可用性、`WebError` | -| [spill.md](spill.md) | spill 存储 seam:`SaveTextSpill`、`SpillOwner`/`SpillSource`、`SpillRef`、品牌类型 `SpillLocator` | -| [workflow.md](workflow.md) | 工作流 seam:`WorkflowStartRequest`、`WorkflowMeta`、`WorkflowRun`/`Result`、`workflow/*` 事件载荷、`WorkflowError` 致命性 | - -> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务使用生成的[服务目录](../cordis-catalog/services.md)。 - - - -## `…Map → derived-union` 模式 - -harness 中几乎所有可扩展的和类型都遵循同一形状:一个以判别标签为键的接口(`…Map`),联合类型由 `keyof` 派生。插件通过**声明合并**添加变体——无需修改拥有该类型的包(package)。 - -```ts ignore-check -// The pattern, schematically: -interface ThingMap { - 'a': { kind: 'a'; /* … */ } - 'b': { kind: 'b'; /* … */ } -} -type ThingKind = keyof ThingMap // 'a' | 'b' -type Thing = ThingMap[keyof ThingMap] // the discriminated union - -// A plugin extends it without touching the source package: -declare module '@deepseek-ai/dsh-llm' { - interface ThingMap { - 'c': { kind: 'c'; /* … */ } - } -} -``` - -五个规范 map 使用此模式;插件作者扩展它们: - -| Map | 包 | 派生 | 目录 | -|---|---|---|---| -| `ContentBlockMap` | dsh-llm | `ContentBlock` | [下文](#content-blocks-and-messages) | -| `MessageSourceMap` | dsh-llm | `MessageSource` | [下文](#content-blocks-and-messages) | -| `FinishReasonMap` | dsh-llm | `FinishReason` | [下文](#the-model-request-and-result) | -| `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.md) | -| `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.md) | - -消费方最常 `switch` 的两个大型判别联合类型是:**`StreamChunk`**(流式协议)和 **`SessionEvent`**(日志条目)。按仓库约定,对标签做 `switch`——不要链式 `if`——这样每个分支都能窄化类型,拼错的标签会编译失败。 - - - -## 品牌化 ID - -跨越包边界的 ID 都经过**品牌化**——结构上是字符串,但在类型层面不可互换(不能把 `SessionId` 传给需要 `CallId` 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。 - -`Branded` 原语位于独立的纯类型包 [dsh-brand](../../packages/util/brand) 中(没有运行时代码,也不依赖 Harness 包),因此任何包都能品牌化其拥有的 id,而无需依赖无关的能力包。 - -源码:[`packages/util/brand/src/index.ts`](../../packages/util/brand/src/index.ts) - -```ts type-equiv -/** A string carrying a compile-time-only brand `B`. */ -type Branded = string & { readonly [BRAND]: B } -``` - -两个核心 ID 是 `CallId`(关联工具调用及其结果;dsh-llm)和 `SessionId`(活跃 agent 与持久会话共享的标识;dsh-session)。能力包也会品牌化各自的 id,例如 [tasks.md](tasks.md) 中的 `TaskId`。 - - - -## 内容块与消息 - -一段对话由 `Message` 组成;一条消息是一个类型化**内容块**的数组。块的联合类型从 `ContentBlockMap` 派生。 - -源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) - -```ts type-equiv -/** - * Merge-extensible content blocks keyed by `type`. New core blocks must land - * with adapter, UI, and compaction support. - */ -interface ContentBlockMap { - 'text': TextBlock - 'reasoning': ReasoningBlock - 'tool-call': ToolCallBlock - 'tool-result': ToolResultBlock -} -``` - -各块接口(完整字段见源码):`TextBlock`(`text`)、`ReasoningBlock`(thinking,区别于可见文本)、`ToolCallBlock`(`id: CallId`、`name`、原始 JSON `arguments`)、`ToolResultBlock`(`toolCallId`、嵌套 `content: ContentBlock[]`、`isError?`)。`ContentBlock = ContentBlockMap[ContentBlockType]`。核心集仅限于每条交付路径都尊重的块——多模态内容(图像、音频等)没有核心块类型;需要的功能通过可合并扩展的 map 添加,同时提供适配器/UI/压缩支持。 - -源码:[`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts) - -`Message` 是一个带标识且不可变的角色/来源/内容值。模型产生的 assistant 消息会在其来源中携带提供方/模型所有权与可选的适配器私有回放元数据: - -```ts type-equiv -/** Provider ownership and adapter-private replay data for an assistant message. */ -interface AssistantProvenance { - /** Provider route that produced the message. */ - provider: string - /** Provider model id that produced the message. */ - model: string - /** - * Lossless-JSON adapter state needed to replay the provider response. - * `LlmService` exposes it to a target adapter only when that adapter instance - * currently owns both this historical provider and the target provider. - */ - replayState?: unknown -} -``` - -```ts type-equiv -/** One immutable message representation shared by delivery, durable history, and model requests. */ -interface Message { - /** Stable identity preserved across every representation boundary. */ - readonly id: MessageId - /** Provider-neutral conversation role. */ - readonly role: 'system' | 'user' | 'assistant' - /** Exact model-facing blocks. */ - readonly content: ContentBlock[] - /** Required producer provenance. */ - readonly source: MessageSource -} -``` - -消息来源本身也是一个可合并扩展的和类型: - -```ts type-equiv -/** - * Where a message (or injected content) came from. - * Merge-extensible sum type — plugins add their own `kind`s. - */ -interface MessageSourceMap { - user: { kind: 'user' } - plugin: { kind: 'plugin'; plugin: string } & ContextFormed - model: ModelMessageSource - tool: ToolMessageSource -} -``` - -溯源与形态是相互独立的两根轴。`kind` 回答「由谁产生」;生产方可选混入的 `form` 回答「这是何种形态的信息」,因此多个生产方可以共用一种呈现,一个生产方在一次会话中也可以发出多种形态。该词汇表是语义的,逐个取值增长;未声明或无法识别的取值是有文档的默认,按不透明内容呈现: - -```ts type-equiv -/** - * What SHAPE of information a producer-supplied context carries, declared by - * the producer beside its provenance. - * - * `MessageSource.kind` answers *who produced this*; `form` answers *what kind - * of thing it is*, and the two axes are deliberately independent — several - * producers share one form (three snapshot producers today), and one producer - * may emit more than one form over a session. - * - * The vocabulary is SEMANTIC, never visual: a value states that the content is - * a file's instructions or a catalog of available items, and a consumer decides - * what that looks like. Colors, icons, ordering, and collapse defaults are the - * consumer's business and must not enter this union. It grows one value at a - * time as producers gain the structured fields their form needs; an absent or - * unknown value is the documented default, presented as opaque content. - */ -type ContextForm = - /** Instructions read out of workspace files the model is expected to follow. */ - | 'instructions' - /** A catalog of items available in this session, republished as it changes. */ - | 'catalog' - /** Current state, where a later snapshot from the same producer supersedes an earlier one. */ - | 'snapshot' - /** A one-off account of something that just happened; it supersedes nothing. */ - | 'notice' - /** A message another agent addressed to this one. */ - | 'relay' - /** Material lifted out of another session's log, possibly reduced on the way in. */ - | 'recall' -``` - -```ts type-equiv -/** One named contribution to a `snapshot`-form context, in assembly order. */ -interface ContextSnapshotSection { - /** The contributing subsystem's name. */ - readonly name: string - /** That contribution's model-facing text, exactly as assembled. */ - readonly text: string -} -``` - -```ts type-equiv -/** - * Producer-declared {@link ContextForm} and the fields that form requires, - * mixed into the source shapes that carry one. - * - * Discriminated by `form` so a producer cannot declare a shape without the - * facts that shape is presented from: a `notice` must record its one-line - * account, a `snapshot` its sections. Omitting `form` stays valid — an - * undeclared context is the documented default. - */ -type ContextFormed = - | { readonly form?: never } - | { readonly form: 'instructions' } - | { readonly form: 'catalog' } - | { - readonly form: 'snapshot' - /** The named contributions this snapshot assembled, in order. */ - readonly sections: readonly ContextSnapshotSection[] - } - | { - readonly form: 'notice' - /** One-line account of what happened, shown without expanding the row. */ - readonly summary: string - } - | { readonly form: 'relay' } - | { readonly form: 'recall' } -``` - -## 流式输出 - -适配器发出原始**分片**协议;循环记录分片(回放保真度),同时将同一批分片送入 `BlockAssembler` 以重建块和消息。`StreamChunk` 是基于 `type` 的封闭判别联合——`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`。 - -完整联合类型、适配器契约(usage-before-finish、原始 JSON 工具参数、两条认可的错误路径)和 `BlockAssembler` 在 **[llm-streaming.md](llm-streaming.md)** 中。 - - - -## 模型请求 - -一次模型调用是一个完全组装好的 `GenerateOptions`。适配器以原始 `StreamChunk` 流作答;消费方用 `BlockAssembler` 组装它(见 [llm-streaming.md](llm-streaming.md))。 - -源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) - -提供方与模型发现使用小型、提供方无关的描述符。模型目录仅供参考:路由仍以已注册提供方为键,适配器也可以接受未列出的模型 id。 - -注册适配器会返回一个句柄:既是释放器,也带有原子的路由替换——路由集合由用户配置决定的插件正需要它。 - -```ts type-equiv -/** - * What {@link LlmService.registerAdapter} returns: the disposer, plus an - * atomic route replacement for the same adapter instance. - */ -interface AdapterRegistrationHandle { - /** Release every route this registration currently holds. */ - (): void - /** - * Replace this registration's routes with `providers`, keeping the same - * adapter instance. The candidate set is validated in full first — a - * conflict with another adapter, an invalid name, or bad provider metadata - * throws and leaves the current routes untouched — and the swap itself is - * one synchronous section, so no request can observe a gap. An empty array - * is legal here (a settings section that emptied holds zero routes while - * staying registered), unlike an empty initial registration. - * - * Throws `LlmError` with code `REGISTRATION_DISPOSED` once the registration - * has been released: its routes are gone and its disposer has already run, - * so anything registered afterwards would have no owner left to release it. - * @param providers - the complete next route set for this registration. - */ - replace(providers: string[]): void -} -``` - -```ts type-equiv -/** Display metadata for one registered provider route. */ -interface LlmProviderInfo { - /** Provider route key used by {@link GenerateOptions.provider}. */ - id: string - /** Human-readable provider name for selectors and diagnostics. */ - name: string -} -``` - -适配器插件还会通过 `registerConfigurableProviders()` 声明哪些路由*可以*运行,并指明每条路由的用户设置分节,使配置界面能在任何路由注册之前就呈现休眠的提供方。 - -```ts type-equiv -/** - * One provider route an adapter plugin can activate through configuration, - * whether or not the route is currently registered. Configuration surfaces - * merge this directory with `listProviders()` to offer every configurable - * provider alongside its live/dormant state. - */ -interface LlmConfigurableProvider { - /** Provider route key this entry activates when configured. */ - provider: string - /** Human-readable provider name for configuration surfaces. */ - displayName: string - /** User-settings namespace whose section configures this provider. */ - settingsNs: string - /** - * Path from that namespace's section root to this provider's profile - * object; empty when the whole section is the profile. - */ - settingsPath: readonly string[] - /** - * Whether the owning adapter knows this route only because configuration - * declared it — a gateway or self-hosted server it ships nothing about. - * Absent means the adapter draws no such distinction; false means it does - * and this route is one of its own. Only the adapter can answer: a stored - * profile is how a user-added route AND a corrected shipped one both look - * from outside. - */ - declared?: boolean -} -``` - -```ts type-equiv -/** One adapter-discovered model; catalog membership is advisory, not request validation. */ -interface LlmModelInfo { - /** Provider route that owns this model entry. */ - provider: string - /** Model id passed to {@link GenerateOptions.model}. */ - id: string - /** Human-readable model name for selectors. */ - name: string - /** Optional user-facing distinction from otherwise similar models. */ - description?: string -} -``` - -界面正在起草的提供方既没有路由也没有 catalog,因此询问被单独描述:请求携带用户正在编辑的草稿,回复是界面可以采纳的候选,而不是它必须服务的 catalog。 - -```ts type-equiv -/** - * One interrogation of a provider endpoint that configuration has not stored - * yet. Configuration surfaces send the draft a user is still editing, so the - * request carries the endpoint and credential directly instead of naming a - * route: a provider being added has no route to name. - */ -interface LlmModelDiscoveryRequest { - /** - * Route the draft is editing, when it edits an existing one. A route whose - * adapter already knows its models answers from that knowledge instead of - * asking the endpoint — the adapter's own registry is the better answer, and - * it costs no network call. - */ - provider?: string - /** - * Endpoint to interrogate. Optional because a route the adapter already - * describes needs none; a route it does not must supply one. - */ - baseURL?: string - /** Wire protocol the endpoint speaks, when the draft names one. */ - api?: string - /** Credential for this interrogation alone; the harness never stores it. */ - apiKey?: string - /** Caller cancellation; implementations must settle promptly after it aborts. */ - signal?: AbortSignal -} -``` - -```ts type-equiv -/** - * One model an endpoint reports about itself. Every field but the id is - * optional because most provider listings disclose an id and nothing else; - * a surface adopting one of these still owes the capacities its adapter needs. - */ -interface LlmDiscoveredModel { - /** Model id the endpoint accepts. */ - id: string - /** Human-readable name when the endpoint supplies one. */ - name?: string - /** Maximum combined request and response context, when disclosed. */ - contextWindow?: number - /** Maximum output tokens, when disclosed. */ - maxTokens?: number -} -``` - -对正确性敏感的元数据与参考目录分开解析,并归服务该确切路由的适配器所有。上下文容量、适配器调用默认值和推理选项共用同一个确切模型结果,消费方因而无需重复执行权威模型解析。 - -```ts type-equiv -/** Provider-owned context capacity for one exact provider/model route. */ -interface LlmModelContext { - /** Maximum combined request and response context in tokens. */ - contextWindow: number -} -``` - -推理强度是另一项针对确切路由的能力。核心为标识符添加品牌类型,但不枚举其值;有序集合、展示名称和可选的部署默认值均由各适配器持有。 - -```ts type-equiv -/** Adapter-owned identifier for one model's selectable reasoning effort. */ -type ReasoningEffortId = Branded<'ReasoningEffortId'> -``` - -```ts type-equiv -/** Display metadata for one adapter-owned reasoning effort. */ -interface LlmReasoningEffortInfo { - /** Opaque stable value accepted by {@link GenerateOptions.reasoningEffort}. */ - id: ReasoningEffortId - /** Human-readable effort name for selectors and diagnostics. */ - name: string - /** Optional user-facing distinction from otherwise similar efforts. */ - description?: string -} -``` - -```ts type-equiv -/** Selectable reasoning efforts for one exact provider/model route. */ -interface LlmModelReasoningInfo { - /** Supported efforts in adapter-preferred display order. */ - efforts: readonly LlmReasoningEffortInfo[] - /** - * Adapter-configured default materialized into requests when callers omit - * an effort. Absence preserves the provider's own default. - */ - defaultEffort?: ReasoningEffortId -} -``` - -```ts type-equiv -/** Exact-route model metadata resolved by its owning adapter. */ -interface LlmResolvedModelInfo extends LlmModelInfo { - /** Provider-owned context capacity when known. */ - context?: LlmModelContext - /** Adapter-configured per-request output cap materialized when callers omit one. */ - defaultMaxTokens?: number - /** Adapter-owned selectable reasoning levels when exposed. */ - reasoning?: LlmModelReasoningInfo -} -``` - -```ts type-equiv -/** A single model request, fully assembled. */ -interface GenerateOptions { - /** Registered provider route selecting the adapter instance. */ - provider: string - model: string - /** Adapter-owned reasoning effort selected for this exact model. */ - reasoningEffort?: ReasoningEffortId - /** - * Ordered conversation messages, exactly as the provider sees them (after - * the `system` slot). A loop-built request assembles them as - * the derived history (dsh-agent-loop); a hand-built one-shot passes any list. - */ - messages: Message[] - /** System prompt text (adapters map to the provider's system slot). */ - system?: string - /** Tool schemas (adapters map to the provider's `tools` field). */ - tools?: ToolSchema[] - temperature?: number - maxTokens?: number - /** - * Stop sequences: generation halts as soon as the model produces any one of - * these strings (adapters map to the provider's stop field, e.g. OpenAI - * `stop`). The stop string itself is not included in the output. - */ - stop?: string[] - signal?: AbortSignal - /** - * Session identity stamped by the loop for listener routing. Adapters ignore - * it; replay uses it to keep concurrent parent and child cursors independent. - */ - sessionId?: Branded<'SessionId'> - /** - * Provider-neutral classification for an auxiliary model call. Adapters may - * map the purpose to model-hidden transport metadata or purpose-specific - * generation policy. Ordinary conversation requests leave it unset. - */ - purpose?: 'compaction' | 'session-title' -} -``` - -模型响应为何停止由可合并扩展的原因表示。提供方终态失败携带流式契约的 [`LlmFailure`](llm-streaming.md#llmfailure): - -```ts type-equiv -/** - * Why a model response stopped. - * Merge-extensible so adapters can surface provider-specific reasons. - */ -interface FinishReasonMap { - 'stop': { kind: 'stop' } - 'tool-calls': { kind: 'tool-calls' } - 'max-tokens': { kind: 'max-tokens' } - 'aborted': { kind: 'aborted'; failure: LlmFailure } - 'error': { kind: 'error'; failure: LlmFailure } -} -``` - -`FinishReason = FinishReasonMap[keyof FinishReasonMap]`。`TokenUsage`(逐调用计量,含不相交的缓存字段)详见 [llm-streaming.md](llm-streaming.md)。 - -`GenerateOptions.tools` 携带 `ToolSchema`——工具的 JSON Schema 描述,发送给模型。它声明在 dsh-llm(而非 dsh-tools)中,正是因为它是循环每一步组装请求的一部分: - -```ts type-equiv -/** - * JSON-schema description of a tool, as sent to the model. - * - * Declared here (not in dsh-tools) because it is part of {@link GenerateOptions}; - * dsh-tools' ToolDefinition and dsh-system-prompt's PromptAssembly both import - * it from this package. - */ -interface ToolSchema { - name: string - description: string - /** JSON Schema object for the arguments. */ - parameters: Record -} -``` - -面向模型的 `ToolSchema` 是协议格式;产出它的已注册 `ToolDefinition`(schema + `execute`)在 [tools.md](tools.md) 中。 - -### 请求信封:`LlmCallConfig` 与记录的 header - -循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、适配器默认值来源、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。 - -`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置及其来源。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。 - -在协议格式上,循环构建的请求先读取 `system` 槽位(渲染后的提示词组装),再读取派生历史——边界快照,其尾部在轮次首步是最新的 `user/message`,在后续步骤是上一步的工具结果。开发不变式针对每个循环构建的请求精确重算此等式。 - -```ts type-equiv -/** - * Provider, model, reasoning effort, and sampling scalars of one conversation's - * requests. Every field maps 1:1 onto the same-named `GenerateOptions` field; - * the loop builds requests from the logged header rather than accepting these - * per call. - */ -interface LlmCallConfig { - provider: string - model: string - reasoningEffort?: ReasoningEffortId - temperature?: number - maxTokens?: number - stop?: string[] -} -``` - -```ts type-equiv -/** - * Effective config fields supplied by exact-model adapter resolution rather - * than by the caller's request proposal. - */ -interface LlmCallConfigAdapterDefaults { - reasoningEffort?: true - maxTokens?: true -} -``` - -## 会话 - -`Session` 是一份类型化 `SessionEvent` 的**仅追加日志**——唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`),而非单独存储。事件词汇从 `SessionEventMap` 派生: - -源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts) - -```ts type-equiv -/** - * One immutable entry in the session log. - * - * A proper discriminated union over `type` (not independent `type`/`data` - * unions), so `switch (event.type)` narrows `event.data` without casts. - * - * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional: - * they only exist on {@link SurfaceEventType} variants (`user/message`, - * `assistant/message`, `tool/result`). - * Non-surface events (boundary markers, chunks, usage, errors) never carry - * surface metadata — the compiler enforces this at `Session.append()` - * call sites. - */ -type SessionEvent = { - [K in SessionEventType]: { - type: K - /** Monotonic sequence number within the session. */ - seq: number - /** Unix epoch milliseconds. */ - time: number - data: SessionEventMap[K] - } & (K extends SurfaceEventType ? { - /** - * Seq numbers of events that are provenance sources of this event - * (e.g. the `assistant/chunk` seqs that built an `assistant/message`, - * or the surface nodes shadowed by a compaction replace node). An - * `assistant/message` may carry a present empty array for a known empty - * provider stream; omission means unrecorded provenance. - */ - sourceEventSeqs?: number[] - /** How this event entered the surface; absent for non-surface events. */ - surfaceOp?: SurfaceOp - } : object) -}[T] -``` - -会话事件变体、`deriveMessages()` 投影规则、`TurnEndReason` 词汇以及执行封闭和独立事件规则都在 **[session.md](session.md)** 中。日志如何持久化——`SessionPersistence` seam、JSONL/SQLite 后端、`session/flush` 检查点、崩溃恢复与 `SessionHeader`——则在 **[persistence.md](persistence.md)** 中。 - - - -## Agent 句柄 - -`Agent` 是每个插件(UI、钩子、orchestrator)面向编程的 surface。具体实现为 dsh-agent-loop 包内部细节;循环外没有任何组件依赖它。 - -源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) - -```ts type-equiv -/** One of the two ordered pending-message lists owned by an agent. */ -type InboxTarget = 'next-turn' | 'next-step' -``` - -每个待处理入队项就是其 `UserMessage`;`MessageId` 是唯一标识。`Inbox.append`、`prepend`、`replace`、`remove`、`clear`、`splice` 与 `claim` 会记录规范化的持久 `agent/inbox/spliced` 变更,并拒绝重复的待处理 id。`replace(messageId, newMessage)` 与 `remove(messageId)` 通过 `MessageId` 跨两份列表定位待处理消息;替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。普通删除和 `clear()` 都表示取消。`claim(target)` 通过无 outcome 的纯删除 splice 移除拟进入步骤的批次——全部 `next-step` 输入,外加轮次边界上的一条 `next-turn` 消息——且不发出 discarded 通知;循环另行逐条发出 claimed 通知。UI 投影等整体队列消费方通过持久 splice 重建 `nextTurn` 与 `nextStep`,而跟踪单条消息的消费方使用精确的 `agent/inbox/inserted`、`claimed` 与 `discarded` 通知。 - -```ts type-equiv -/** Options for {@link Agent.cancel}. */ -interface CancelOptions { - /** - * Preserve queued and steering inbox items instead of discarding them. The - * active turn is still aborted, but un-started and pending work survives for a - * later turn and no canceled inbox splice is logged. - */ - keepInbox?: boolean | undefined -} -``` - -```ts type-equiv -/** Why an active agent driver was cancelled. */ -type AgentCancelCause = - | { readonly kind: 'user' } - | { readonly kind: 'parent' } - | { readonly kind: 'hook'; readonly reason: string } - | { readonly kind: 'disposed' } -``` - -`Agent` 是覆盖公开活跃 agent 契约的接口。它的统一 `send` 方法直接公开目标与唤醒路由;`followup`、`steer` 和 `inject` 是固定预设别名。 - -```ts type-equiv -/** Public live-agent handle. */ -interface Agent { - /** The single identity shared with {@link session}. */ - readonly id: SessionId - /** The provider route and model this agent's requests use. */ - readonly options: AgentOptions - /** The live session this agent drives; its log is the durable source of truth. */ - readonly session: Session - /** The agent-owned projection of durable pending work. */ - readonly inbox: Inbox - /** The current lifecycle state, mirrored on every `agent/status` transition. */ - readonly status: AgentStatus - /** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */ - readonly ctx: Context - - /** - * Clear queued and steering work — unless `keepInbox` — and abort the active - * turn or between-turn task. The first cause wins for that activity. With no - * active activity, cancellation is a no-op and does not arm later work. - * @param cause - the stable caller intent carried by the active operation signal. - * @param options - cancellation options; `keepInbox` preserves pending work. - */ - cancel(cause: AgentCancelCause, options?: CancelOptions): void - - /** - * Resolve after the current whole-agent activity reaches quiescence. This - * follows replacement work started before the observed driver retires, - * but does not identify the settlement of any particular message. - * @returns fulfillment after no active driver or maintenance task remains. - */ - whenIdle(): Promise - - /** - * Run one non-turn maintenance task from the true idle phase. The task starts - * synchronously after claiming that phase; later waking input remains in the - * inbox until the task settles, while public status stays `idle`. - * `whenIdle()` follows both the task and any waking work released behind it. - * @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}. - * @throws synchronously when turn-driving or another maintenance task already owns the agent. - * @returns the task promise. - */ - runMaintenance(task: (signal: AbortSignal) => Promise): Promise - - /** - * Route identified input to an inbox boundary and optionally wake the driver. - * Waking input submitted after active cancellation is queued for the next - * turn and runs when the aborted activity converges to idle; a `disposed` - * cancel leaves it parked. A wake submitted while already idle always opens - * its turn boundary, even when its message is cleared before the driver - * claims ([cancel-convergence wake latch](../../../../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)). - * @param message - identified content and its producer provenance. - * @param target - the preferred next-turn or next-step inbox boundary. - * @param wakeup - whether delivery may wake the driver. - */ - send(message: UserMessage, target: InboxTarget, wakeup: boolean): void - - /** - * Queue an ordinary follow-up turn and wake the driver. The item becomes the - * sole ordinary message of its own turn. - * @param message - identified prompt content and its producer provenance. - */ - followup(message: UserMessage): void - - /** - * Submit steering for the nearest step. An idle driver starts a turn; - * a running driver consumes it at its next step boundary. - * A rejected step leaves steering parked in the inbox until the next - * wake; cancellation or disposal may discard pending steering. - * @param message - identified steering content and its producer provenance. - */ - steer(message: UserMessage): void - - /** - * Queue model-facing context for the next pre-step without waking the - * driver. A running driver claims it at the nearest later step boundary; - * idle drivers leave it pending until follow-up or steering - * wakes them. It may miss a request whose pre-step already claimed its - * batch. Cancellation or disposal may discard pending context. - * @param message - identified injected context and its producer provenance. - */ - inject(message: UserMessage): void -} -``` - -`AgentStatus` 为 `'idle' | 'running'`,`SessionId` 是品牌类型。dispose(资源释放)会把 agent 从注册表移除并发出 `agent/disposed`;它不是一个终态 status 值。`running` 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。`followup()` 不返回 handle:其 `MessageId` 标识持久 inbox 的插入、领取与丢弃事实,而不标识之后的助手输出或轮次结束。`whenIdle()` 观察整个 agent,因此只有显式拥有从回执到 idle 这一完整区间的调用方才能将其称为一次运行([决策](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md))。`AgentOptions` 可合并扩展:core 声明 `provider?`、`model?` 与 `maxTokens?`(在 `agent/request` 后,分发要求 provider 与 model 都存在)。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。Persona 归 `dsh-system-prompt` 所有:agent 作用域的 `deployment:persona` 可以遮蔽全局默认值。 - -cause 是由 TypeScript 强制约束的同进程输入。活跃的取消持有者会将它复制到仅运行时的 `AbortSignal.reason`;signal 不授予协作监听器任何分类权限。持久 `turn/end` 保留粗粒度 `{ kind: 'aborted' }` 结果;若需记录请求 provenance,应使用单独的持久事件,而不是让终态结果承担额外含义。 - -[事件分类](../architecture.md#event)拥有 `agent/*` 生命周期、检查点与 waterfall(瀑布式事件)契约。轮次和步骤边界是持久会话事件,而不是 agent emit。 - -## 发起 Agent - -`ctx.agents` 携带的进程本地 initiator 就是上面的确切 `Agent`,不是单独的 frame 或复制的标识。环境中存在该值既不能证明存活,也不代表授权;其生命周期与边界规则由 [initiator 作用域决策](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)规定。 - -## 拦截决策 - -pre-step 决策使用与持久 user-role 输入相同、带标识的 `UserMessage` 形状。进入步骤的批次具有权威性,并保留每条消息的标识与 provenance。钩子桥接层把其原生决策字段映射到这一类型化结果上。 - -源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) - -`agent/pre-step` 接收一个 payload,携带独占的已领取批次(`messages`)、拟进入步骤的坐标(`turn`、`step`)与当前轮次的取消 `signal`。首次提案在已打开的轮次内、任何步骤开始前运行;工具 continuation 可以在步骤之间提交空的已领取批次: - -它返回 `PreStepDecision`。reject 不会打开步骤。enter 提供在 `step/start` 后追加的完整消息批次;最终决策省略的已领取消息保持已删除,而领取后插入的输入仍留待后续处理: - -```ts type-equiv -/** Whether and with which messages the loop enters a proposed step. */ -type PreStepDecision = - | { kind: 'reject' } - | { kind: 'enter'; messages: UserMessage[] } -``` - -`agent/request-error` 在失败的模型步骤关闭之后、其轮次关闭之前运行。listener 可以在失败轮次的 signal 仍然存活时修复持久状态或 await 策略工作。处理该错误的 listener 返回 `{ kind: 'retry' }` 且不调用 `next()`;默认的 `undefined` 会让失败保持终态。 - -```ts type-equiv -/** Action returned by a listener that owns model-request recovery. */ -type RequestErrorAction = { kind: 'retry' } | undefined -``` - -`agent/pre-step` 是请求推导前唯一的串行边界。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。 - -`agent/session-start` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart): - -```ts type-equiv -/** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */ -type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact' -``` - -## `ToolDefinition` - -唯一属于核心的流水线编写类型:每个已注册工具*是什么*——一个面向模型的 `ToolSchema` 加上一个 `execute` 函数,以及可选的最终内容回调与 UI 回调。工具作者很少手动构造它(`defineTool` DSL 会用类型化参数构建),但它是注册表持有、循环分发所经过的契约。 - -其完整字段、`defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` 类型化 schema DSL、`ToolExecution`/`ToolExecutionResult` waterfall 形状,以及工具展示 UI 词汇在 **[tools.md](tools.md)** 中。 diff --git a/docs/core-data-structures/credentials.i18n.yaml b/docs/core-data-structures/credentials.i18n.yaml deleted file mode 100644 index d44275d97e..0000000000 --- a/docs/core-data-structures/credentials.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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/credentials.md -credentials.md: ef74ddeb4346e18f8d5d33488657e5d50f1d754e -credentials.zh.md: 09cf374a2346fd93aa834e3372321e6eeece6ed8 diff --git a/docs/core-data-structures/credentials.md b/docs/core-data-structures/credentials.md deleted file mode 100644 index ef74ddeb43..0000000000 --- a/docs/core-data-structures/credentials.md +++ /dev/null @@ -1,50 +0,0 @@ -# User Credentials - -English | [中文](credentials.zh.md) - -The credential seam of [dsh-credentials](../../packages/credentials/credentials) keeps secrets out of configuration: settings sections and `cordis.yml` entries carry *references* (environment-variable names), providers such as [dsh-credentials-local](../../packages/credentials/credentials-local) own the values, and consumers resolve a reference once per operation — the LLM adapters resolve once per model request, so a rotated credential reaches the very next request without any restart. One seam-wide rule binds every provider: an empty stored value is absent everywhere. - -Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts) - -## Identity - -A reference names one credential as a POSIX-style environment-variable name. The brand keeps references from mixing with other cross-boundary strings; construction validates the shell-identifier shape. - -```ts type-equiv -/** Nominal reference to one credential: a POSIX-style environment-variable name. */ -type CredentialRef = Branded<'CredentialRef'> -``` - -## Resolution - -`resolve(ref)` returns the value with the provider-defined source layer that supplied it, or `undefined` while unconfigured. Consumers re-resolve at each operation and never cache across operations — that per-operation read is the hot-update mechanism. - -```ts type-equiv -/** One resolved credential value and the source layer that supplied it. */ -interface ResolvedCredential { - /** The non-empty secret value. */ - value: string - /** Provider-defined source layer id (the local provider uses `env`, `file`, `project-env`, and `user-env`). */ - source: string -} -``` - -## Description - -`describe(ref)` answers configuration surfaces without ever exposing a value: whether the reference resolves, from which layer, and whether `set` would currently succeed. The local provider reports a reference supplied by the live process environment as `writable: false` — a write would appear to succeed while resolution kept returning the shadowing value, so the seam rejects it and the UI can render the reference read-only up front. - -```ts type-equiv -/** Source and writability facts for one reference, safe for configuration UIs — never the value. */ -interface CredentialInfo { - /** Whether {@link Credentials.resolve} would currently return a value. */ - configured: boolean - /** Source layer currently supplying the value; absent while unconfigured. */ - source?: string - /** Whether {@link Credentials.set} would currently succeed for this reference. */ - writable: boolean -} -``` - -## 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. diff --git a/docs/core-data-structures/credentials.zh.md b/docs/core-data-structures/credentials.zh.md deleted file mode 100644 index 09cf374a23..0000000000 --- a/docs/core-data-structures/credentials.zh.md +++ /dev/null @@ -1,50 +0,0 @@ -# 用户凭据 - -[English](credentials.md) | 中文 - -[dsh-credentials](../../packages/credentials/credentials) 的凭据 seam 把机密挡在配置之外:settings 分节与 `cordis.yml` 条目携带的是*引用*(环境变量名),值归 [dsh-credentials-local](../../packages/credentials/credentials-local) 这类 provider 所有,消费方每个操作解析一次引用——LLM 适配器每次模型请求解析一次,因此轮换后的凭据无需任何重启即可作用于紧随其后的下一次请求。一条 seam 级规则约束每个 provider:空的存储值在任何地方都视为不存在。 - -Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts) - -## 标识 - -引用以 POSIX 风格环境变量名命名一条凭据。brand 使引用不与其他跨边界字符串混用;构造时校验 shell 标识符形态。 - -```ts type-equiv -/** Nominal reference to one credential: a POSIX-style environment-variable name. */ -type CredentialRef = Branded<'CredentialRef'> -``` - -## 解析 - -`resolve(ref)` 返回值,连同供出该值、由 provider 定义的来源层;未配置期间返回 `undefined`。消费方在每个操作中重新解析,绝不跨操作缓存——这次按操作进行的读取正是热更新机制。 - -```ts type-equiv -/** One resolved credential value and the source layer that supplied it. */ -interface ResolvedCredential { - /** The non-empty secret value. */ - value: string - /** Provider-defined source layer id (the local provider uses `env`, `file`, `project-env`, and `user-env`). */ - source: string -} -``` - -## 描述 - -`describe(ref)` 在绝不暴露值的前提下回应配置界面:引用当前是否可解析、来自哪一层、`set` 当前能否成功。本地 provider 把由活跃进程环境供值的引用报告为 `writable: false`——那样的写入会表面成功而解析持续返回遮蔽值,因此 seam 直接拒绝,界面也得以提前把该引用渲染为只读。 - -```ts type-equiv -/** Source and writability facts for one reference, safe for configuration UIs — never the value. */ -interface CredentialInfo { - /** Whether {@link Credentials.resolve} would currently return a value. */ - configured: boolean - /** Source layer currently supplying the value; absent while unconfigured. */ - source?: string - /** Whether {@link Credentials.set} would currently succeed for this reference. */ - writable: boolean -} -``` - -## 变更提交 - -`credentials/updated (ref)` 在 provider 管理的来源发生已提交变更后触发——`set`、`unset` 或在存储中观察到的外部编辑。进程环境自身的变化不可观测,永不发出事件。消费方不需要该事件(它们按操作重新解析);它服务于配置界面刷新「已配置」徽标。 diff --git a/docs/core-data-structures/filesystem.i18n.yaml b/docs/core-data-structures/filesystem.i18n.yaml deleted file mode 100644 index 2f9478a7ba..0000000000 --- a/docs/core-data-structures/filesystem.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/core-data-structures/llm-streaming.i18n.yaml b/docs/core-data-structures/llm-streaming.i18n.yaml deleted file mode 100644 index 7168f9bd85..0000000000 --- a/docs/core-data-structures/llm-streaming.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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/llm-streaming.md -llm-streaming.md: 5c90b3ce4ac65a99997f6ba7ad5deac494b7f772 -llm-streaming.zh.md: 7fd0043234cb40d6b21cec6ff101993164785a6e diff --git a/docs/core-data-structures/llm-streaming.md b/docs/core-data-structures/llm-streaming.md deleted file mode 100644 index 5c90b3ce4a..0000000000 --- a/docs/core-data-structures/llm-streaming.md +++ /dev/null @@ -1,257 +0,0 @@ -# LLM Streaming - -English | [中文](llm-streaming.zh.md) - -The wire-level streaming vocabulary of [dsh-llm](../../packages/llm/llm). [core.md](core.md) introduces `StreamChunk`, `Message`, and `ContentBlock`; this page owns the full chunk protocol, the adapter contract every adapter must obey, and the shared assembler. - -Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) - -## `StreamChunk` — the raw protocol - -A streaming response interleaves several typed blocks (text, reasoning, multiple tool calls). `index` ties each delta to its block; `block-end` carries the fully-assembled `ContentBlock` so consumers don't have to re-assemble deltas themselves. It is a **closed** discriminated union — a `switch` over `type` ends with `assertNever`, so adding a variant breaks compilation at every consumer that must handle it. - -```ts type-equiv -/** - * Raw streaming protocol emitted by adapters. - * Block indexes correlate interleaved deltas, and `block-end` carries the - * assembled block. Adapters emit usage before the terminal finish and nothing - * afterward; tool arguments remain raw JSON strings. An adapter implementation - * may throw, but `LlmService.stream()` normalizes that failure to a terminal - * `error` or `aborted` finish before exposing it to consumers. - */ -type StreamChunk = - | { type: 'block-start'; index: number; blockType: ContentBlockType } - | { type: 'text-delta'; index: number; text: string } - | { type: 'reasoning-delta'; index: number; text: string } - | { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string } - | { type: 'block-end'; index: number; block: ContentBlock } - | { type: 'usage'; usage: TokenUsage } - | { - type: 'finish' - reason: FinishReason - /** Adapter-private lossless-JSON state for replaying a successful response. */ - replayState?: unknown - } -``` - -## `LlmFailure` - -Every thrown or in-band final-adapter failure normalizes to one serializable provider-neutral payload. `providerRetryAfterMs` is a validated positive delay requested by the provider, not a retry decision; `ProviderRequestId` is an opaque branded string for diagnostics. - -```ts type-equiv -/** Serializable provider-boundary facts; policy decides whether they are retryable. */ -interface LlmFailure { - /** Human-readable provider or transport failure. */ - readonly message: string - /** Stable provider-neutral machine-routing code. */ - readonly code: string - /** HTTP status observed at the provider boundary, when available. */ - readonly status?: number - /** Provider-requested delay in milliseconds, when valid and available. */ - readonly providerRetryAfterMs?: number - /** Opaque provider-issued request identifier for diagnostics. */ - readonly requestId?: ProviderRequestId -} -``` - -## The adapter contract - -Every adapter MUST obey these, and every consumer may rely on them: - -- **`usage` before `finish`, nothing after `finish`.** Defer both to the provider's end-of-stream marker so a trailing usage-only chunk can't violate the ordering. -- **Tool-call `arguments` stay raw JSON strings end-to-end.** Partial fragments stream via `argumentsDelta`; a provider that hands back parsed objects re-stringifies at `block-end`. -- **Two sanctioned error paths, one fact shape.** A failure may either THROW from `stream()` (transport/protocol errors) **or** end the stream with `finish {kind:'error'|'aborted', failure}` (provider in-band errors, for adapters that can't throw mid-stream). `LlmError.failure` carries the same `LlmFailure`. The final adapter boundary preserves the exact thrown `Error` object and associates immutable facts plus the serving registration's immutable retry policy with that call; the agent loop closes the failed step and offers the error, facts, immutable prior-retried facts, serving policy, and turn signal to `agent/request-error`. A handling listener returns `{ kind: 'retry' }` after its awaited repair; absent recovery the structured failure becomes the turn error, and no normal assistant message or tool side effect is committed for that attempt. -- **One adapter call is one provider attempt.** Adapters disable library retries. Agent-level recovery opens another durable numbered turn; direct `ctx.llm.stream()` callers remain single-attempt. -- **Provider stalls are bounded at the transport.** Both shipping remote adapters expose positive finite `streamIdleTimeoutMs` with a five-minute default. The watchdog arms only while iterator `next()` is outstanding, uses one stable signal for the whole request, maps its own expiry to `TIMEOUT`, and keeps an earlier caller abort as `ABORTED`. -- **Context overflow has one canonical code.** Both DeepSeek adapters classify explicit provider detail through `isContextWindowExceededError()` and surface `CONTEXT_WINDOW_EXCEEDED`, whether the failure arrives as a thrown HTTP `LlmError` or an in-band finish error. Consumers route on the code, never provider text. -- **An empty completion is a retryable error, not a silent success.** Both adapters map a terminal `stop` finish that carried no content blocks to `finish {kind:'error'}` with the canonical `EMPTY_RESPONSE` code, and `dsh-llm-retry` retries it by default; see [empty model responses are retryable](../../.agents/notes/implemented/bug-fix/2026-07-24-empty-model-response-is-retryable.md). -- **Every provider HTTP request carries the app-attribution header.** Adapters send `attributionHeaders()` (below), the `User-Agent` baseline. -- **Replay state is adapter-owned.** A successful `finish` may carry lossless-JSON state needed to reconstruct a native provider response. The loop stores it with the assembled assistant message. On a later request, `LlmService` passes the state only when the historical provider and target provider are currently registered to the exact same adapter instance. That adapter validates the state and owns any cross-model or cross-provider conversion; other adapters receive the provider-neutral content and provenance without the private state. - -Two independent implementations obey this contract: `dsh-llm-deepseek` uses direct fetch with SSE framing through `eventsource-parser`, while `dsh-llm-pi-ai` provides a generic multi-provider adapter through `@earendil-works/pi-ai`. Both carry cancellation and the idle watchdog to the provider request. - -## `ResolvedRetryPolicy` - -Provider configuration resolves before route registration into an immutable discriminated union. Normal mode carries `mode: 'normal'`, finite `maxRetries`, `retryableCodes`, and required `initialDelayMs`, `maxDelayMs`, and `jitterRatio`; always mode carries `mode: 'always'` and the same required backoff fields without a finite maximum. `LlmService.providerRetryPolicy(provider)` returns the currently registered value and supplies normal defaults when the adapter omits one; `llmRetryPolicyOf(stream)` returns the exact serving registration's captured value after that call enters its final adapter boundary, so later route disposal or replacement cannot change an in-flight failure's recovery policy. The [generated config catalog](../config-catalog.md) owns the optional input shapes. - -## `AppIdentity` — app attribution - -The static public application identity every adapter sends to providers ([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts)). `attributionHeaders(identity?)` maps it to the standard `User-Agent` header only; OpenRouter-specific app attribution headers are intentionally not supported by this contract. The default `APP_IDENTITY` sources its version from the package manifest; every field is a public product fact - no secrets, paths, session ids, or per-user identifiers, and nothing per-request may influence the values. Rationale: [Mandatory `User-Agent` attribution](../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md). - -```ts type-equiv -/** - * Static public application identity sent to LLM providers. - * - * Every field is a public product fact, safe on every request: no secrets, - * local paths, session ids, prompt text, or per-user identifiers belong here, - * and nothing per-request may influence the values. - */ -interface AppIdentity { - /** `User-Agent` product token (lowercase, hyphenated). */ - product: string - /** Product version; sourced from package metadata, never hand-copied. */ - version: string - /** Public home URL of the app, used as the `User-Agent` comment. */ - url: string -} -``` - -## `TokenUsage` - -Per-call token accounting. Counts are **disjoint**: `inputTokens` is uncached input only; cached input is reported separately, and billed input is the sum of the three. Adapters whose providers fold cache hits into a single prompt total (DeepSeek's `prompt_tokens`) subtract them back out. `reasoningTokens`, when present, is informational detail already included in `outputTokens`; totals must not add it again. - -```ts type-equiv -/** - * Token accounting for one model call (cache fields are optional). - * - * Counts are DISJOINT: `inputTokens` is uncached input only; cached input is - * reported separately as `cacheReadTokens`/`cacheWriteTokens` (billed input = - * sum of the three). Adapters whose providers fold cache hits into a total - * prompt count (DeepSeek's `prompt_tokens`) subtract them out. - */ -interface TokenUsage { - inputTokens: number - outputTokens: number - cacheReadTokens?: number - cacheWriteTokens?: number - reasoningTokens?: number -} -``` - -## `BlockAssembler` - -`BlockAssembler` ([`packages/llm/llm/src/assembler.ts`](../../packages/llm/llm/src/assembler.ts)) is the single shared implementation that folds a `StreamChunk` stream back into `ContentBlock`s, usage, finish reason, and replay state. The loop logs the raw chunks while feeding the same chunks through an assembler, then stores the assembled assistant content with its provider/model provenance. A consumer that needs the assembled result without re-implementing the fold uses this. - -```ts public-api -/** - * Incrementally assembles raw {@link StreamChunk}s into complete - * {@link ContentBlock}s and a final assistant {@link Message}. - * - * The agent loop feeds it while logging raw chunks for replay fidelity, then - * reads `blocks()` / `message()` / `usage` / `finish` once the stream ends. - * - * Tolerant of delta-only protocols (no block-start/end); deltas arriving for - * an index already closed by `block-end` are ignored (malformed stream) so a - * misbehaving adapter cannot grow memory or corrupt a completed block. - */ -declare class BlockAssembler { - /** - * Feed one chunk into the assembly state. - * @param chunk - the next raw chunk, in stream order. - */ - push(chunk: StreamChunk): void; - /** - * Assemble all blocks seen so far, in stream order. - * @returns one block per seen index, except that max-token truncation drops - * tool calls that cannot be executed safely; an open block assembles from - * its accumulated deltas (an unknown block type never closed by `block-end` throws). - */ - blocks(): ContentBlock[]; - /** Usage from the `usage` chunk; undefined until one arrives. */ - get usage(): TokenUsage | undefined; - /** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */ - get finish(): FinishReason; - /** Adapter-private replay state from the terminal finish chunk, if any. */ - get replayState(): unknown; - /** - * The assembled assistant message. - * @param source - producer attribution for the assembled message. - * @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules). - */ - message(source: MessageSource = { kind: 'plugin', plugin: 'dsh-llm/assembler' }): Message; -} -``` - -## The seam - -`LlmAdapter` is the provider seam: subclass, implement `stream()`, and register one adapter instance with `ctx.llm.registerAdapter(providers, adapter)`. `GenerateOptions.provider` selects the registered adapter; `GenerateOptions.model` is passed to that adapter and need not be registered at lifecycle start. Duplicate provider routes fail atomically. Optional `providerRetryPolicy()` is captured per route with normal defaults, while `providerInfo()` and asynchronous `listModels()` feed `LlmService.listProviders()` / `listModels()` with detached selector metadata. That catalog is advisory rather than a request whitelist: the adapter remains authoritative and may accept unlisted model ids. One asynchronous `resolveModel()` query returns exact model identity plus optional correctness-sensitive context capacity, an adapter-configured `defaultMaxTokens`, and ordered model-owned reasoning ids with an optional deployment default; absent fields mean unavailable metadata or provider-owned behavior, not invalid catalog membership. The resolver receives optional cancellation and must settle promptly after abort. `LlmService.resolveModelInfo()` validates and detaches the aggregate. At the final adapter boundary, `resolveCallConfig()` materializes the output default only when `maxTokens` is absent and validates and materializes reasoning, so direct calls cannot bypass either configured behavior; direct dispatch captures one registration before awaiting that resolution. The agent loop instead uses `prepareCall()` to keep the same registration across model resolution, durable header logging, and dispatch, retain detached context metadata from that exact lookup, and report which config fields the adapter defaulted. Adapter lookup happens at the terminal continuation of the `llm/stream` waterfall, so a listener may short-circuit the call or route a mutable one-shot request before lookup. AgentLoop observes a request attempt once the outer waterfall returns a stream handle; that limited boundary does not prove a lazy terminal adapter was constructed or began provider I/O. The `block-start` / `block-end` `index` correlation and the assembler together mean an adapter only has to emit well-formed chunks — block reassembly is not each adapter's problem. The consumer surface (`ctx.llm.stream()`) and the `llm/stream` waterfall are described in [architecture.md § Content blocks and streaming](../architecture.md#content-blocks-and-streaming-dsh-llm). - -```ts type-equiv -/** One model call whose config and adapter registration were resolved together. */ -interface PreparedLlmCall { - /** Detached, deep-frozen config with any adapter-owned default materialized. */ - readonly config: LlmCallConfig - /** Immutable retry policy captured with the adapter registration. */ - readonly retryPolicy: ResolvedRetryPolicy - /** Detached context metadata resolved with the registration-bound call. */ - readonly context?: LlmModelContext - /** Config fields materialized by the captured adapter rather than proposed by the caller. */ - readonly adapterDefaults: LlmCallConfigAdapterDefaults - /** - * Dispatch this call once through the registration captured during - * preparation. The request's call-config fields must match {@link config}; - * reuse or mismatch fails with `INVALID_PREPARED_CALL`. - * @param options - fully assembled request carrying the prepared config. - * @returns the chunk stream, including the `llm/stream` waterfall. - */ - stream(options: GenerateOptions): AsyncIterable -} -``` - -```ts public-api -/** - * Provider-wire adapter for the harness message and stream vocabulary. Register implementations - * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include - * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The direct-fetch - * DeepSeek and library-backed pi-ai adapters intentionally exercise this contract through different internals. - */ -declare abstract class LlmAdapter { - /** - * Describe one provider route owned by this adapter. - * @param provider - a route passed to `registerAdapter()` for this instance. - * @returns detached display metadata whose id must equal `provider`. - */ - providerInfo(provider: string): LlmProviderInfo; - /** - * Return the provider-owned retry policy captured with this route. - * @param _provider - a route passed to `registerAdapter()` for this instance. - * @returns a resolved policy, or `undefined` to use the normal defaults. - */ - providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined; - /** - * List models this adapter can currently advertise for one owned provider. - * The result is advisory: an adapter may accept unlisted model ids, and - * consumers must not turn absence into request rejection. - * @param _provider - one provider route owned by this adapter. - * @returns discoverable models in adapter-preferred order. - */ - listModels(_provider: string): Promise; - /** - * Resolve all metadata available for one exact model. This query is - * independent of the advisory catalog and does not validate request routing. - * @param provider - one provider route owned by this adapter. - * @param model - exact model id passed to {@link GenerateOptions.model}. - * @param _signal - cancellation for this exact-model lookup; asynchronous - * implementations must settle promptly after it aborts. - * @returns provider/model identity plus any context, call-default, and reasoning metadata. - */ - resolveModel( - provider: string, - model: string, - _signal?: AbortSignal, - ): Promise; - /** - * Stream one model call as raw chunks. The only required method. - * @param options - the fully-assembled request; implementations must honor `options.signal`. - * @returns the chunk stream, obeying the adapter contract documented on `StreamChunk`. - */ - abstract stream(options: GenerateOptions): AsyncIterable; -} -``` - -`ContentBlockType` (the key set the `index`-correlated blocks carry) derives from `ContentBlockMap`: - -```ts type-equiv -/** - * Merge-extensible content blocks keyed by `type`. New core blocks must land - * with adapter, UI, and compaction support. - */ -interface ContentBlockMap { - 'text': TextBlock - 'reasoning': ReasoningBlock - 'tool-call': ToolCallBlock - 'tool-result': ToolResultBlock -} -``` - -See [core.md § Content blocks and messages](core.md#content-blocks-and-messages) for the block interfaces. diff --git a/docs/core-data-structures/llm-streaming.zh.md b/docs/core-data-structures/llm-streaming.zh.md deleted file mode 100644 index 7fd0043234..0000000000 --- a/docs/core-data-structures/llm-streaming.zh.md +++ /dev/null @@ -1,257 +0,0 @@ -# LLM(大语言模型)流式输出 - -[English](llm-streaming.md) | 中文 - -[dsh-llm](../../packages/llm/llm) 的协议格式(wire format)级流式输出词汇。[core.md](core.md) 介绍了 `StreamChunk`、`Message` 与 `ContentBlock`;本页拥有完整的分片协议、每个适配器必须遵守的适配器契约(adapter contract),以及共享的 assembler。 - -源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) - -## `StreamChunk`:原始协议 - -一个流式响应交错包含多种类型的块(文本、推理(reasoning)、多个工具调用)。`index` 将每个 delta 关联到其所属块;`block-end` 携带完整组装好的 `ContentBlock`,消费方无需自行重新组装 delta。这是一个**封闭的**可辨识联合类型:对 `type` 的 `switch` 以 `assertNever` 结尾,因此新增变体会在每个必须处理它的消费方处触发编译错误。 - -```ts type-equiv -/** - * Raw streaming protocol emitted by adapters. - * Block indexes correlate interleaved deltas, and `block-end` carries the - * assembled block. Adapters emit usage before the terminal finish and nothing - * afterward; tool arguments remain raw JSON strings. An adapter implementation - * may throw, but `LlmService.stream()` normalizes that failure to a terminal - * `error` or `aborted` finish before exposing it to consumers. - */ -type StreamChunk = - | { type: 'block-start'; index: number; blockType: ContentBlockType } - | { type: 'text-delta'; index: number; text: string } - | { type: 'reasoning-delta'; index: number; text: string } - | { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string } - | { type: 'block-end'; index: number; block: ContentBlock } - | { type: 'usage'; usage: TokenUsage } - | { - type: 'finish' - reason: FinishReason - /** Adapter-private lossless-JSON state for replaying a successful response. */ - replayState?: unknown - } -``` - -## `LlmFailure` - -每个抛出的失败或最终适配器的带内失败都会规范化为一种可序列化、提供方无关的 payload。`providerRetryAfterMs` 是经校验、由提供方请求的正数延迟,而不是重试决策;`ProviderRequestId` 是用于诊断的不透明品牌字符串。 - -```ts type-equiv -/** Serializable provider-boundary facts; policy decides whether they are retryable. */ -interface LlmFailure { - /** Human-readable provider or transport failure. */ - readonly message: string - /** Stable provider-neutral machine-routing code. */ - readonly code: string - /** HTTP status observed at the provider boundary, when available. */ - readonly status?: number - /** Provider-requested delay in milliseconds, when valid and available. */ - readonly providerRetryAfterMs?: number - /** Opaque provider-issued request identifier for diagnostics. */ - readonly requestId?: ProviderRequestId -} -``` - -## 适配器契约 - -每个适配器必须遵守以下规则,每个消费方可以依赖它们: - -- **`usage` 在 `finish` 之前,`finish` 之后不再有任何分片。** 将两者都推迟到提供方的流结束标记,这样尾部的 usage-only 分片就不会违反顺序。 -- **工具调用的 `arguments` 全程保持原始 JSON 字符串。** 部分片段通过 `argumentsDelta` 流式传输;如果提供方返回的是已解析的对象,适配器在 `block-end` 时重新序列化为字符串。 -- **两条受支持的错误路径,一种事实形状。** 失败可以从 `stream()` 抛出(传输/协议错误),**或者**以 `finish {kind:'error'|'aborted', failure}` 结束流(无法在流中途抛异常的适配器用它表示提供方带内错误)。`LlmError.failure` 携带同一个 `LlmFailure`。最终适配器边界保留被抛出的确切 `Error` 对象,并将不可变事实以及实际服务注册所对应的不可变重试策略关联到该调用;agent loop(智能体循环)关闭失败步骤,再把错误、事实、不可变的先前已重试失败事实、实际服务策略和轮次信号提供给 `agent/request-error`。处理该错误的 listener 在其 await 的修复完成后返回 `{ kind: 'retry' }`;若未恢复,结构化失败会成为轮次错误,并且该次尝试不会提交正常 assistant 消息或工具副作用。 -- **一次适配器调用就是一次提供方尝试。** 适配器禁用库重试。agent 层恢复会打开另一个持久、带编号的轮次;直接调用 `ctx.llm.stream()` 的调用方仍然只尝试一次。 -- **提供方停顿在传输层受到时限约束。** 两个已交付的远程适配器都暴露正数且有限的 `streamIdleTimeoutMs`,默认五分钟。watchdog 只在 iterator `next()` 尚未完成时启动,整个请求使用同一个稳定 signal,把自身到期映射为 `TIMEOUT`,并把更早发生的调用方中止保留为 `ABORTED`。 -- **上下文溢出只有一个规范 code。** 两个 DeepSeek 适配器都通过 `isContextWindowExceededError()` 对提供方的显式细节分类并暴露 `CONTEXT_WINDOW_EXCEEDED`,无论失败以抛出的 HTTP `LlmError` 还是带内 finish error 到达。消费方按 code 路由,绝不依赖提供方文本。 -- **空 completion 是可重试错误,而不是静默的成功结果。** 两个适配器都把没有携带任何内容块的终止性 `stop` 结束映射为携带规范 `EMPTY_RESPONSE` code 的 `finish {kind:'error'}`,`dsh-llm-retry` 默认会重试它;详见[空模型响应可重试](../../.agents/notes/implemented/bug-fix/2026-07-24-empty-model-response-is-retryable.md)。 -- **每个提供方 HTTP 请求都携带应用归属头。** 适配器发送下文的 `attributionHeaders()`,即 `User-Agent` 基线。 -- **回放状态归适配器所有。** 成功的 `finish` 可以携带重建提供方原生响应所需的无损 JSON 状态。循环会将其与组装后的 assistant 消息一起存储。后续请求中,仅当历史提供方与目标提供方当前注册到完全相同的适配器实例时,`LlmService` 才会传递该状态。该适配器负责校验状态并拥有所有跨模型或跨提供方转换;其他适配器只会收到提供方无关的内容与 provenance,不会收到私有状态。 - -两个彼此独立的实现遵循该契约:`dsh-llm-deepseek` 使用直接 fetch,并通过 `eventsource-parser` 进行 SSE(Server-Sent Events)分帧;`dsh-llm-pi-ai` 则通过 `@earendil-works/pi-ai` 提供通用多提供方适配器。两者都会把取消与空闲 watchdog 传递至提供方请求。 - -## `ResolvedRetryPolicy` - -提供方配置会在路由注册前解析为不可变的可辨识联合。normal mode 携带 `mode: 'normal'`、有限的 `maxRetries`、`retryableCodes`,以及必填的 `initialDelayMs`、`maxDelayMs` 与 `jitterRatio`;always mode 携带 `mode: 'always'` 和相同的必填退避字段,但没有有限上限。`LlmService.providerRetryPolicy(provider)` 返回当前注册的值,并在适配器省略策略时提供 normal 默认值;调用进入最终适配器边界后,`llmRetryPolicyOf(stream)` 返回为其提供服务的确切注册所捕获的值,因此之后释放或替换路由都无法改变进行中失败的恢复策略。可选输入形状由[生成的配置目录](../config-catalog.md)规定。 - -## `AppIdentity`:应用归属 - -每个适配器都会向提供方发送的静态公开应用标识([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts))。`attributionHeaders(identity?)` 只把它映射到标准 `User-Agent` header;该契约有意不支持 OpenRouter 特有的应用归属 header。默认 `APP_IDENTITY` 从包 manifest(元数据清单)获取版本;每个字段都是公开产品事实——不含 secret、路径、会话 id 或逐用户标识,且任何逐请求信息都不得影响这些值。设计理由见[强制 `User-Agent` 归属](../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。 - -```ts type-equiv -/** - * Static public application identity sent to LLM providers. - * - * Every field is a public product fact, safe on every request: no secrets, - * local paths, session ids, prompt text, or per-user identifiers belong here, - * and nothing per-request may influence the values. - */ -interface AppIdentity { - /** `User-Agent` product token (lowercase, hyphenated). */ - product: string - /** Product version; sourced from package metadata, never hand-copied. */ - version: string - /** Public home URL of the app, used as the `User-Agent` comment. */ - url: string -} -``` - -## `TokenUsage` - -逐调用 token 记账。各计数**互不重叠**:`inputTokens` 只包含未缓存输入;缓存输入单独报告,计费输入是三者之和。若提供方把缓存命中折入单一提示词总数(如 DeepSeek 的 `prompt_tokens`),适配器会再将其扣除。`reasoningTokens` 存在时只是信息性细节,已经包含在 `outputTokens` 中;汇总时不得重复相加。 - -```ts type-equiv -/** - * Token accounting for one model call (cache fields are optional). - * - * Counts are DISJOINT: `inputTokens` is uncached input only; cached input is - * reported separately as `cacheReadTokens`/`cacheWriteTokens` (billed input = - * sum of the three). Adapters whose providers fold cache hits into a total - * prompt count (DeepSeek's `prompt_tokens`) subtract them out. - */ -interface TokenUsage { - inputTokens: number - outputTokens: number - cacheReadTokens?: number - cacheWriteTokens?: number - reasoningTokens?: number -} -``` - -## `BlockAssembler` - -`BlockAssembler`([`packages/llm/llm/src/assembler.ts`](../../packages/llm/llm/src/assembler.ts))是唯一的共享实现,负责把 `StreamChunk` 流折叠回 `ContentBlock`、usage、结束原因与回放状态。循环在记录原始分片的同时,把同一批分片送入 assembler,再将组装后的 assistant 内容连同其提供方/模型 provenance 一起存储。需要组装结果、又不想重新实现 fold 的消费方使用它。 - -```ts public-api -/** - * Incrementally assembles raw {@link StreamChunk}s into complete - * {@link ContentBlock}s and a final assistant {@link Message}. - * - * The agent loop feeds it while logging raw chunks for replay fidelity, then - * reads `blocks()` / `message()` / `usage` / `finish` once the stream ends. - * - * Tolerant of delta-only protocols (no block-start/end); deltas arriving for - * an index already closed by `block-end` are ignored (malformed stream) so a - * misbehaving adapter cannot grow memory or corrupt a completed block. - */ -declare class BlockAssembler { - /** - * Feed one chunk into the assembly state. - * @param chunk - the next raw chunk, in stream order. - */ - push(chunk: StreamChunk): void; - /** - * Assemble all blocks seen so far, in stream order. - * @returns one block per seen index, except that max-token truncation drops - * tool calls that cannot be executed safely; an open block assembles from - * its accumulated deltas (an unknown block type never closed by `block-end` throws). - */ - blocks(): ContentBlock[]; - /** Usage from the `usage` chunk; undefined until one arrives. */ - get usage(): TokenUsage | undefined; - /** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */ - get finish(): FinishReason; - /** Adapter-private replay state from the terminal finish chunk, if any. */ - get replayState(): unknown; - /** - * The assembled assistant message. - * @param source - producer attribution for the assembled message. - * @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules). - */ - message(source: MessageSource = { kind: 'plugin', plugin: 'dsh-llm/assembler' }): Message; -} -``` - -## seam - -`LlmAdapter` 是提供方 seam:创建子类、实现 `stream()`,再用 `ctx.llm.registerAdapter(providers, adapter)` 注册一个适配器实例。`GenerateOptions.provider` 选择已注册适配器;`GenerateOptions.model` 会传给该适配器,无需在生命周期启动时注册。重复提供方路由会原子失败。可选的 `providerRetryPolicy()` 会按路由捕获并填入 normal 默认值,`providerInfo()` 与异步 `listModels()` 方法则为 `LlmService.listProviders()` / `listModels()` 提供分离的 selector 元数据。该目录仅供参考,不是请求白名单:适配器仍是权威,并可接受未列出的模型 id。单次异步 `resolveModel()` 查询返回确切模型身份,以及可选的对正确性敏感的上下文容量、适配器配置的 `defaultMaxTokens`、由模型持有的有序推理强度 ID 和部署默认值;字段缺失表示元数据不可用或保留提供方持有的行为,而不表示目录成员关系无效。解析器会接收可选的取消信号,并且必须在信号中止后迅速完成结算。`LlmService.resolveModelInfo()` 会校验聚合结果并返回分离值。在最终适配器边界,`resolveCallConfig()` 仅在 `maxTokens` 缺失时填入输出默认值,并校验和填入推理强度,因此直接调用也无法绕过任何一项已配置行为;直接分派会在等待解析前捕获一项适配器注册。agent loop 则使用 `prepareCall()`,使模型解析、请求头持久记录和分派全程使用同一项注册,保留来自同一次查询的分离上下文元数据,并报告适配器填入的配置字段。适配器查找发生在 `llm/stream` waterfall(瀑布式事件)的终端 continuation,因此 listener 可以在查找前短路调用,或路由一个可变的一次性请求。AgentLoop 在外层 waterfall 返回流句柄时观察到一次请求尝试;这个有限边界不能证明惰性终端适配器已构造完成或开始提供方 I/O。`block-start` / `block-end` 的 `index` 关联与 assembler 共同意味着适配器只需 emit 格式正确的分片——块重组不是每个适配器各自的问题。消费方 surface(`ctx.llm.stream()`)与 `llm/stream` waterfall 见 [architecture.md § 内容块与流式传输](../architecture.md#content-blocks-and-streaming-dsh-llm)。 - -```ts type-equiv -/** One model call whose config and adapter registration were resolved together. */ -interface PreparedLlmCall { - /** Detached, deep-frozen config with any adapter-owned default materialized. */ - readonly config: LlmCallConfig - /** Immutable retry policy captured with the adapter registration. */ - readonly retryPolicy: ResolvedRetryPolicy - /** Detached context metadata resolved with the registration-bound call. */ - readonly context?: LlmModelContext - /** Config fields materialized by the captured adapter rather than proposed by the caller. */ - readonly adapterDefaults: LlmCallConfigAdapterDefaults - /** - * Dispatch this call once through the registration captured during - * preparation. The request's call-config fields must match {@link config}; - * reuse or mismatch fails with `INVALID_PREPARED_CALL`. - * @param options - fully assembled request carrying the prepared config. - * @returns the chunk stream, including the `llm/stream` waterfall. - */ - stream(options: GenerateOptions): AsyncIterable -} -``` - -```ts public-api -/** - * Provider-wire adapter for the harness message and stream vocabulary. Register implementations - * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include - * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The direct-fetch - * DeepSeek and library-backed pi-ai adapters intentionally exercise this contract through different internals. - */ -declare abstract class LlmAdapter { - /** - * Describe one provider route owned by this adapter. - * @param provider - a route passed to `registerAdapter()` for this instance. - * @returns detached display metadata whose id must equal `provider`. - */ - providerInfo(provider: string): LlmProviderInfo; - /** - * Return the provider-owned retry policy captured with this route. - * @param _provider - a route passed to `registerAdapter()` for this instance. - * @returns a resolved policy, or `undefined` to use the normal defaults. - */ - providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined; - /** - * List models this adapter can currently advertise for one owned provider. - * The result is advisory: an adapter may accept unlisted model ids, and - * consumers must not turn absence into request rejection. - * @param _provider - one provider route owned by this adapter. - * @returns discoverable models in adapter-preferred order. - */ - listModels(_provider: string): Promise; - /** - * Resolve all metadata available for one exact model. This query is - * independent of the advisory catalog and does not validate request routing. - * @param provider - one provider route owned by this adapter. - * @param model - exact model id passed to {@link GenerateOptions.model}. - * @param _signal - cancellation for this exact-model lookup; asynchronous - * implementations must settle promptly after it aborts. - * @returns provider/model identity plus any context, call-default, and reasoning metadata. - */ - resolveModel( - provider: string, - model: string, - _signal?: AbortSignal, - ): Promise; - /** - * Stream one model call as raw chunks. The only required method. - * @param options - the fully-assembled request; implementations must honor `options.signal`. - * @returns the chunk stream, obeying the adapter contract documented on `StreamChunk`. - */ - abstract stream(options: GenerateOptions): AsyncIterable; -} -``` - -`ContentBlockType`(`index` 关联块所携带的键集合)派生自 `ContentBlockMap`: - -```ts type-equiv -/** - * Merge-extensible content blocks keyed by `type`. New core blocks must land - * with adapter, UI, and compaction support. - */ -interface ContentBlockMap { - 'text': TextBlock - 'reasoning': ReasoningBlock - 'tool-call': ToolCallBlock - 'tool-result': ToolResultBlock -} -``` - -块接口详见 [core.md § Content blocks and messages](core.md#content-blocks-and-messages)。 diff --git a/docs/core-data-structures/persistence.i18n.yaml b/docs/core-data-structures/persistence.i18n.yaml deleted file mode 100644 index 09965f293f..0000000000 --- a/docs/core-data-structures/persistence.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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/persistence.md -persistence.md: aa95acfb4c2a859ab5c4d72633653e37e9a80795 -persistence.zh.md: e16420ee4984a27da14c96b90a0c799fc042a34f diff --git a/docs/core-data-structures/sandbox.i18n.yaml b/docs/core-data-structures/sandbox.i18n.yaml deleted file mode 100644 index 34691ad25b..0000000000 --- a/docs/core-data-structures/sandbox.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/core-data-structures/session-query.i18n.yaml b/docs/core-data-structures/session-query.i18n.yaml deleted file mode 100644 index eca4715ba9..0000000000 --- a/docs/core-data-structures/session-query.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/core-data-structures/session-reference.i18n.yaml b/docs/core-data-structures/session-reference.i18n.yaml deleted file mode 100644 index 85bbcf8877..0000000000 --- a/docs/core-data-structures/session-reference.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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-reference.md -session-reference.md: 5375677f6a1748909743ca76d5191cb9e736a40a -session-reference.zh.md: 3ff4a1719926bda0a9111482a7778a8c94553370 diff --git a/docs/core-data-structures/session-title.i18n.yaml b/docs/core-data-structures/session-title.i18n.yaml deleted file mode 100644 index 67f9ac765b..0000000000 --- a/docs/core-data-structures/session-title.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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-title.md -session-title.md: fff1aa1f6be45d0cfc4d7f6a9527ccb93561618f -session-title.zh.md: 77a0a5e94053c94bf84bbc749cb6e260898b5d00 diff --git a/docs/core-data-structures/session.i18n.yaml b/docs/core-data-structures/session.i18n.yaml deleted file mode 100644 index 16e969bd94..0000000000 --- a/docs/core-data-structures/session.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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.md -session.md: 5d54b03df8ec3345e8bf04702f242e3aacf8ec39 -session.zh.md: 5a867cfe302bf02994bfe5a5a704bed55222e8ea diff --git a/docs/core-data-structures/settings.i18n.yaml b/docs/core-data-structures/settings.i18n.yaml deleted file mode 100644 index cf262dc1e9..0000000000 --- a/docs/core-data-structures/settings.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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/settings.md -settings.md: bd01c1d28407af9cab26f624a054a010e25a3ddd -settings.zh.md: 1cb7f8b507f29f2b6876fd48b4c37284df235e53 diff --git a/docs/core-data-structures/skills.i18n.yaml b/docs/core-data-structures/skills.i18n.yaml deleted file mode 100644 index d0ce7b7574..0000000000 --- a/docs/core-data-structures/skills.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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/skills.md -skills.md: d862cbc07135680c2377b4c8c82c80d055344221 -skills.zh.md: 3f8c034ec2aa24b4acdbbc1e2ac27717c21649d5 diff --git a/docs/core-data-structures/spill.i18n.yaml b/docs/core-data-structures/spill.i18n.yaml deleted file mode 100644 index 17f210bc85..0000000000 --- a/docs/core-data-structures/spill.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/core-data-structures/subagent.i18n.yaml b/docs/core-data-structures/subagent.i18n.yaml deleted file mode 100644 index 57b960da91..0000000000 --- a/docs/core-data-structures/subagent.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/core-data-structures/subprocess.i18n.yaml b/docs/core-data-structures/subprocess.i18n.yaml deleted file mode 100644 index b2d38854b2..0000000000 --- a/docs/core-data-structures/subprocess.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/core-data-structures/system-prompt.i18n.yaml b/docs/core-data-structures/system-prompt.i18n.yaml deleted file mode 100644 index 8d9cd0897f..0000000000 --- a/docs/core-data-structures/system-prompt.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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/system-prompt.md -system-prompt.md: 59193c1881abcadbc8a1778cde92f5a6572eee24 -system-prompt.zh.md: 41e45417817895ccf6def70e510eca7422a65e41 diff --git a/docs/core-data-structures/system-prompt.md b/docs/core-data-structures/system-prompt.md deleted file mode 100644 index 59193c1881..0000000000 --- a/docs/core-data-structures/system-prompt.md +++ /dev/null @@ -1,78 +0,0 @@ -# System Prompt Assembly - -English | [中文](system-prompt.zh.md) - -The [system-prompt package](../../packages/core/system-prompt) owns the data exchanged between prompt contributors and one assembly call. The package [README](../../packages/core/system-prompt/README.md) documents registration, ordering, scoping, and rendering behavior; this page pins the literal cross-package shapes that plugins implement or pass. - -Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts). - -## Assembly context - -`AssembleContext` identifies the scope layer one assembly resolves and may carry the explicit control signal for that request. It is merge-extensible: `dsh-agent` adds the optional live `agent` field, and `assembleContextFor(agent, signal)` sets the explicit fields together. A bare assembly has neither scope nor signal. - -```ts type-equiv -/** Merge-extensible context for one prompt assembly. */ -interface AssembleContext { - /** - * Scope whose providers and waterfall listeners participate. When absent, - * only global providers and subject-less listeners participate. - */ - scope?: ScopeKey - /** Explicit control signal for the turn that requested this assembly, when any. */ - signal?: AbortSignal -} -``` - -## Tool-provider result - -`ToolProviderResult.schemas` is the model-visible set for the current assembly. `knownNames` is the provider's pre-restriction name universe used to distinguish a configured-name typo from a known tool that is deliberately hidden in this scope. - -```ts type-equiv -/** Tool schemas visible in one assembly and their pre-restriction name set. */ -interface ToolProviderResult { - /** The schemas this provider contributes to THIS assembly. */ - readonly schemas: readonly ToolSchema[] - /** The pre-restriction name universe for config validation (defaults to `schemas`' names). */ - readonly knownNames?: readonly string[] -} -``` - -## Prompt sections - -`PromptSection` is a readonly same-process registration contract. Its text may be static or resolved from the current assembly context. - -```ts type-equiv -/** One contributed section of the system prompt (registry input). */ -interface PromptSection { - /** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */ - readonly name: string - /** - * Sections are concatenated in ascending order. Convention: `-100` is the - * harness identity, `0` the deployment persona, tool guidance uses 100–199; - * other negative orders also render before the persona. - */ - readonly order: number - /** - * Static text or a provider evaluated at each assembly with that assembly's - * {@link AssembleContext}. The text may reference `{{variable}}`s — they are - * interpolated later, by {@link renderPrompt}. - */ - readonly text: string | ((context: AssembleContext) => string) -} -``` - -## Dynamic prompt context - -`PromptContext` is the cache-safe counterpart to `PromptSection`. The assembly resolves and orders these contributions, while agent-loop logs their complete current snapshot after retained model history only when it changed or compaction removed it. - -```ts type-equiv -/** Dynamic model context materialized as a durable user-role snapshot. */ -interface PromptContext { - /** Unique name — a duplicate registration throws (see {@link SystemPrompt.context}). */ - readonly name: string - /** Contexts are joined in ascending order. */ - readonly order: number - /** Static text or a provider evaluated for each assembly. Empty text contributes nothing. */ - readonly text: string | ((context: AssembleContext) => string) -} -``` diff --git a/docs/core-data-structures/system-prompt.zh.md b/docs/core-data-structures/system-prompt.zh.md deleted file mode 100644 index 41e4541781..0000000000 --- a/docs/core-data-structures/system-prompt.zh.md +++ /dev/null @@ -1,78 +0,0 @@ -# 系统提示词组装 - -[English](system-prompt.md) | 中文 - -[system-prompt 包](../../packages/core/system-prompt)负责管理提示词贡献者与一次组装调用之间交换的数据。该包的 [README](../../packages/core/system-prompt/README.md) 记录了注册、排序、作用域与渲染行为;本页固定各插件实现或传递的跨包字面形状。 - -源码:[`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)。 - -## 组装上下文 - -`AssembleContext` 标识一次组装所解析的作用域层,并可携带该请求的显式控制信号。它可合并扩展:`dsh-agent` 添加可选字段 `agent`,用于携带当前的 agent(智能体)实例;`assembleContextFor(agent, signal)` 则一起设置这些显式字段。裸组装既没有作用域,也没有信号。 - -```ts type-equiv -/** Merge-extensible context for one prompt assembly. */ -interface AssembleContext { - /** - * Scope whose providers and waterfall listeners participate. When absent, - * only global providers and subject-less listeners participate. - */ - scope?: ScopeKey - /** Explicit control signal for the turn that requested this assembly, when any. */ - signal?: AbortSignal -} -``` - -## 工具提供方结果 - -`ToolProviderResult.schemas` 是当前组装中对模型可见的工具 schema 集合。`knownNames` 是提供方在限制前的名称全集,用于区分「配置名拼写错误」与「已知工具在此作用域中被有意隐藏」。 - -```ts type-equiv -/** Tool schemas visible in one assembly and their pre-restriction name set. */ -interface ToolProviderResult { - /** The schemas this provider contributes to THIS assembly. */ - readonly schemas: readonly ToolSchema[] - /** The pre-restriction name universe for config validation (defaults to `schemas`' names). */ - readonly knownNames?: readonly string[] -} -``` - -## 提示词段落 - -`PromptSection` 是一份只读的同进程注册契约。其文本可以是静态的,也可以从当前组装上下文动态解析。 - -```ts type-equiv -/** One contributed section of the system prompt (registry input). */ -interface PromptSection { - /** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */ - readonly name: string - /** - * Sections are concatenated in ascending order. Convention: `-100` is the - * harness identity, `0` the deployment persona, tool guidance uses 100–199; - * other negative orders also render before the persona. - */ - readonly order: number - /** - * Static text or a provider evaluated at each assembly with that assembly's - * {@link AssembleContext}. The text may reference `{{variable}}`s — they are - * interpolated later, by {@link renderPrompt}. - */ - readonly text: string | ((context: AssembleContext) => string) -} -``` - -## 动态提示词上下文 - -`PromptContext` 是与 `PromptSection` 对应的缓存安全结构。组装会解析这些贡献并排序;agent loop(智能体循环)仅在完整当前快照发生变化或被压缩(compaction)移除时,才会将其记录在保留的模型历史之后。 - -```ts type-equiv -/** Dynamic model context materialized as a durable user-role snapshot. */ -interface PromptContext { - /** Unique name — a duplicate registration throws (see {@link SystemPrompt.context}). */ - readonly name: string - /** Contexts are joined in ascending order. */ - readonly order: number - /** Static text or a provider evaluated for each assembly. Empty text contributes nothing. */ - readonly text: string | ((context: AssembleContext) => string) -} -``` diff --git a/docs/core-data-structures/tasks.i18n.yaml b/docs/core-data-structures/tasks.i18n.yaml deleted file mode 100644 index 58920a53df..0000000000 --- a/docs/core-data-structures/tasks.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/core-data-structures/token-meter.i18n.yaml b/docs/core-data-structures/token-meter.i18n.yaml deleted file mode 100644 index 3f58d10153..0000000000 --- a/docs/core-data-structures/token-meter.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/core-data-structures/token-meter.zh.md b/docs/core-data-structures/token-meter.zh.md deleted file mode 100644 index 0474d86188..0000000000 --- a/docs/core-data-structures/token-meter.zh.md +++ /dev/null @@ -1,43 +0,0 @@ -# Token 计量 - -[English](token-meter.md) | 中文 - -`@deepseek-ai/dsh-token-meter` 公开一个独立的回放快照,用于表示请求压力与按位置计算的表层定价。`logRevision` 表示生成该计量中每个字段时所消费的持久事件数量。 - -来源:[`packages/llm/token-meter/src/types.ts`](../../packages/llm/token-meter/src/types.ts) - -## `TokenMeasurement` - -```ts type-equiv -/** Detached immutable request-pressure and surface snapshot at one consumed log revision. */ -interface TokenMeasurement { - /** Number of durable events consumed; equal to the next unread event seq. */ - readonly logRevision: number - /** Provider or heuristic anchor used for this measurement. */ - readonly baseline: TokenMeasurementBaseline - /** Signed repricing of current surface content relative to the baseline anchor. */ - readonly surfaceDeltaTokens: number - /** Non-negative current request-and-response pressure. */ - readonly totalTokens: number - /** Total heuristic tokens across the current surface. */ - readonly surfaceTokens: number - /** Current surface nodes in positional head-to-tail order. */ - readonly nodes: readonly TokenSurfaceNode[] -} -``` - -`baseline.kind === 'usage'` 表示最近一次成功的提供方调用具有相同的规范请求信封,且该调用的总量不低于其完整启发式锚点。`estimated` 表示不存在可复用的保守 usage 锚点,因此服务使用固定启发式规则对完整信封和表层定价。后续成功请求会替换早先的锚点;有符号的 `surfaceDeltaTokens` 会保留相对于匹配锚点的增长与缩减。`totalTokens` 仍表示请求与响应压力,`surfaceTokens` 则是仅针对表层的启发式总量,等于所有节点价格之和。 - -## `TokenSurfaceNode` - -```ts type-equiv -/** One token-priced node in the current ordered session surface. */ -interface TokenSurfaceNode { - /** Durable sequence number of the surface event. */ - readonly seq: number - /** Heuristic tokens for the exact message projected by this node. */ - readonly tokens: number -} -``` - -表层顺序具有权威性;替换节点的持久 seq 可能高于位置排在其后的节点。该快照不可变,不会随底层回放折叠推进而增长。 diff --git a/docs/core-data-structures/tools.i18n.yaml b/docs/core-data-structures/tools.i18n.yaml deleted file mode 100644 index cd2add6a82..0000000000 --- a/docs/core-data-structures/tools.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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/tools.md -tools.md: 853eed16cff451edcc32bc3aa5c6bc7cabb0f518 -tools.zh.md: ec8809f6bebd185404828c5c5879f8eff832ea9a diff --git a/docs/core-data-structures/typert.i18n.yaml b/docs/core-data-structures/typert.i18n.yaml deleted file mode 100644 index 75b7837687..0000000000 --- a/docs/core-data-structures/typert.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/core-data-structures/user-interaction.i18n.yaml b/docs/core-data-structures/user-interaction.i18n.yaml deleted file mode 100644 index 5480052231..0000000000 --- a/docs/core-data-structures/user-interaction.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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/user-interaction.md -user-interaction.md: 73727cbb754f10633d3213f67f7c3c9fdb215500 -user-interaction.zh.md: b6e95af4026e58c63c713d87ee9a86b3712b311f diff --git a/docs/core-data-structures/workflow.i18n.yaml b/docs/core-data-structures/workflow.i18n.yaml deleted file mode 100644 index 7f996a1c8e..0000000000 --- a/docs/core-data-structures/workflow.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index 676c78bb90..00673a9415 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.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/development.md -development.md: a9edd80abfbd53c9fc0495b4b495fa495207ca95 -development.zh.md: 405942c26ef3600209bc14a300c94a98d4f2b9f3 +development.md: f7084403b30e8a6d12c4ef2b886f8fb61ed39b26 +development.zh.md: 9d3606cf741f25318c630e08e8ac84ad5ee8d345 diff --git a/docs/development.md b/docs/development.md index a9edd80abf..f7084403b3 100644 --- a/docs/development.md +++ b/docs/development.md @@ -21,9 +21,9 @@ Install dependencies from the repo root: pnpm install ``` -The install also configures worktree-local lefthook hooks through `scripts/install-lefthook.mjs`. The [worktree-local hooks Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) owns the safety and migration contract. +The install also configures worktree-local Lefthook hooks and the `dsh-translation-pairing` Git merge driver through `scripts/install-lefthook.mjs`. The [worktree-local hooks Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) owns the hook-path safety contract; the [automatic pairing merges Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) owns the merge driver. -If hooks are missing because dependencies were restored from cache or `postinstall` was skipped, install them manually: +If either integration is missing because dependencies were restored from cache or `postinstall` was skipped, install them manually: ```sh node scripts/install-lefthook.mjs @@ -98,18 +98,23 @@ DEEPSEEK_BASE_URL=https://... # optional `DEEPSEEK_BASE_URL` is optional and defaults to the public API. Never commit real credentials. The real-API e2e suites self-skip when `DEEPSEEK_API_KEY` is not set. -### Git hooks +### Git integrations + +The pairing merge driver derives a conflicted `.i18n.yaml` record from the confirmed ancestor, current, and other owner blobs when both language files use Git's default text strategy and merge cleanly. It fails closed on owner conflicts, non-text merge configuration, or invalid records; after an already-stopped merge, run `pnpm run resolve-translation-pairing-conflicts`, which stages every safe pairing record and exits unsuccessfully if other pairing conflicts still need manual work. See the [bilingual documentation contract](i18n/README.md#the-pairing-contract) for the exact boundary. + +The installer probes the exact Node/tsx driver entrypoint before publishing its worktree configuration. If that runtime later becomes unavailable, the Node-independent launcher writes Git's ordinary text result, leaves the sidecar unresolved, and prints the recovery path; restore dependencies and run `pnpm run resolve-translation-pairing-conflicts`, or run `git merge --abort`. If `pre-merge-commit` rejects an otherwise clean merge, Git leaves the complete result staged without a commit; repair the failure and run `git commit`, or abort. The [automatic pairing merges Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md#failure-contract) owns the exact index and `MERGE_HEAD` states. lefthook is configured in `lefthook.yml` as a fast local checkpoint: -- `pre-commit` applies formatting-only ESLint fixes, validates the staged files with the project-free `.oxlintrc.staged.json` profile and applies Oxlint's native fixes, regenerates `THIRD_PARTY_NOTICES.md` when a staged file is one of its inputs, checks the staged diff for whitespace errors, and runs the vendor manifest guard. +- `pre-commit` verifies staged pairing records against the staged owner blobs, applies formatting-only ESLint fixes, validates the staged files with the project-free `.oxlintrc.staged.json` profile and applies Oxlint's native fixes, regenerates `THIRD_PARTY_NOTICES.md` when a staged file is one of its inputs, checks the staged diff for whitespace errors, and runs the vendor manifest guard. +- `pre-merge-commit` performs the same index-backed pairing check before Git creates an automatic merge commit. - `pre-push` runs `pnpm run typecheck`, which completes the Host lib phase, including generated TypeRT contracts, before the Client TypeScript check. The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code. -The hooks intentionally do not run tests, snapshots, documentation checks, builds, or hygiene. Contributors run the [checks relevant to the changed behavior](../AGENTS.md#run-relevant-checks-locally) once; CI owns exhaustive coverage, built-artifact smokes, and the Node 22.19, 24, and 26 compatibility matrix. +Apart from the scoped staged-record verification, the hooks intentionally do not run tests, snapshots, documentation checks, builds, or hygiene. Contributors run the [checks relevant to the changed behavior](../AGENTS.md#run-relevant-checks-locally) once; CI owns exhaustive coverage, built-artifact smokes, and the Node 22.19, 24, and 26 compatibility matrix. -Contributors can opt into the comprehensive local gate set with `pnpm run check:all`. The command is independent of both Git hooks and is not an agent instruction. +Contributors can opt into the comprehensive local gate set with `pnpm run check:all`. The command is independent of the Git hooks and is not an agent instruction. ### CI gates @@ -151,10 +156,10 @@ Pick the tag that matches the urgency so anyone scanning the code can tell a rel ### Documenting types verbatim (`ts type-equiv`) -The [core data structures](core-data-structures/core.md) docs paste source-equivalent declarations together with their original JSDoc so a reader sees the exact shape and source contract. To keep a paste from drifting when source changes, fence it as ` ```ts type-equiv ` (instead of ` ```ts `) and register it in `scripts/type-equiv.manifest.json` with the source file and symbol it mirrors: +The [subsystems](subsystems/README.md) pages paste source-equivalent declarations together with their original JSDoc so a reader sees the exact shape and source contract. To keep a paste from drifting when source changes, fence it as ` ```ts type-equiv ` (instead of ` ```ts `) and register it in `scripts/type-equiv.manifest.json` with the source file and symbol it mirrors: ```json -{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" } +{ "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" } ``` `pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact shape. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change. diff --git a/docs/development.zh.md b/docs/development.zh.md index 405942c26e..9d3606cf74 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -21,9 +21,9 @@ pnpm install ``` -安装过程还会通过 `scripts/install-lefthook.mjs` 配置 worktree 本地的 lefthook 钩子。其安全与迁移契约由 [worktree 本地钩子 Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) 负责。 +安装过程还会通过 `scripts/install-lefthook.mjs` 配置 worktree 本地的 Lefthook 钩子和 `dsh-translation-pairing` Git 合并驱动。[worktree 本地钩子 Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) 负责钩子路径的安全契约;[自动配对合并 Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) 负责合并驱动。 -如果依赖是从缓存恢复或 `postinstall` 被跳过而导致缺少钩子,请手动安装: +如果依赖是从缓存恢复或 `postinstall` 被跳过而导致任一集成缺失,请手动安装: ```sh node scripts/install-lefthook.mjs @@ -98,18 +98,23 @@ DEEPSEEK_BASE_URL=https://... # optional `DEEPSEEK_BASE_URL` 可选,默认为公开 API。请勿提交真实凭证。未设置 `DEEPSEEK_API_KEY` 时,真实 API 的 e2e 套件会自动跳过。 -### Git 钩子 +### Git 集成 + +当两种语言的文件都使用 Git 默认文本策略且能干净合并时,配对合并驱动会根据已确认的祖先、当前和另一侧的配对文档 blob,推导出发生冲突的 `.i18n.yaml` 记录。配对文档发生冲突、存在非文本合并配置或记录无效时,它会拒绝处理并保留冲突;如果合并已经因冲突而停止,请运行 `pnpm run resolve-translation-pairing-conflicts`,该命令会暂存每份可安全生成的配对记录;如果其他配对冲突仍需手工处理,则以非零状态退出。确切边界见[双语文档契约](i18n/README.md#the-pairing-contract)。 + +安装脚本在发布 worktree 配置前,会探测确切的 Node/tsx 驱动入口点。如果该运行时之后变得不可用,不依赖 Node 的启动器会写入 Git 的普通文本合并结果、让伴随文件保持未解决状态,并打印恢复路径;请恢复依赖后运行 `pnpm run resolve-translation-pairing-conflicts`,或运行 `git merge --abort`。如果 `pre-merge-commit` 拒绝原本能干净完成的合并,Git 会把完整结果留在暂存区但不创建提交;请修复失败后运行 `git commit`,或中止合并。确切的索引与 `MERGE_HEAD` 状态由[自动配对合并 Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md#failure-contract)负责记录。 lefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点: -- `pre-commit` 应用仅用于格式化的 ESLint 修复,使用不加载项目的 `.oxlintrc.staged.json` 配置验证暂存文件并应用 Oxlint 的原生修复,在暂存文件属于 `THIRD_PARTY_NOTICES.md` 的输入时重新生成该文件,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫; +- `pre-commit` 对照暂存的配对文档 blob 校验暂存的配对记录,应用仅用于格式化的 ESLint 修复,使用不加载项目的 `.oxlintrc.staged.json` 配置验证暂存文件并应用 Oxlint 的原生修复,在暂存文件属于 `THIRD_PARTY_NOTICES.md` 的输入时重新生成该文件,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫; +- `pre-merge-commit` 在 Git 创建自动合并提交前执行同样以索引为准的配对检查; - `pre-push` 运行 `pnpm run typecheck`;该命令会先完成包含 TypeRT 契约生成的完整 Host lib 阶段,再运行 Client TypeScript 检查。 vendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md`。 -这些钩子有意不运行测试、快照、文档检查、构建或 `hygiene`。贡献者只运行一次[与改动行为相关的检查](../AGENTS.md#run-relevant-checks-locally);CI 负责全量覆盖率门禁、构建产物冒烟测试,以及 Node 22.19、24 和 26 兼容性矩阵。 +除限定范围的暂存记录校验外,这些钩子有意不运行测试、快照、文档检查、构建或 `hygiene`。贡献者只运行一次[与改动行为相关的检查](../AGENTS.md#run-relevant-checks-locally);CI 负责全量覆盖率门禁、构建产物冒烟测试,以及 Node 22.19、24 和 26 兼容性矩阵。 -贡献者可以选择运行 `pnpm run check:all`,执行全面的本地门禁集。该命令独立于两个 Git 钩子,也不是对 agent 的指令。 +贡献者可以选择运行 `pnpm run check:all`,执行全面的本地门禁集。该命令独立于 Git 钩子,也不是对 agent 的指令。 ### CI 门禁 @@ -151,10 +156,10 @@ pnpm run demo:acp ### 逐字记录类型(`ts type-equiv`) -[核心数据结构](core-data-structures/core.md)文档会把与源码等价的声明及其原始 JSDoc 一并粘贴,让读者看到确切形状和源码契约。为防止粘贴内容在源码变化时漂移,请将其围栏为 ` ```ts type-equiv `(而不是 ` ```ts `),并在 `scripts/type-equiv.manifest.json` 中登记它镜像的源文件和符号: +[子系统](subsystems/README.md)页面会把与源码等价的声明及其原始 JSDoc 一并粘贴,让读者看到确切形状和源码契约。为防止粘贴内容在源码变化时漂移,请将其围栏为 ` ```ts type-equiv `(而不是 ` ```ts `),并在 `scripts/type-equiv.manifest.json` 中登记它镜像的源文件和符号: ```json -{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" } +{ "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" } ``` `pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码契约和确切形状。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index ea2371972f..5bc0fc7fcf 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -10,18 +10,18 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:182`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent/created` | `emit` | [`packages/core/agent/src/types.ts:158`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session) | | `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:167`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | -| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:289`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/telemetry/session-telemetry) | +| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:289`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/types.ts:196`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/types.ts:204`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/types.ts:185`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/types.ts:230`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/types.ts:230`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) | | `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:243`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:259`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | | `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:216`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:177`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compact-basic`](../packages/compact/compact-basic), [`goal-session`](../packages/goal/goal-session), [`headless`](../packages/bundle/headless), [`jsonrpc`](../packages/ui/jsonrpc) | +| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:177`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compact-basic`](../packages/compact/compact-basic), [`goal-session`](../packages/goal/goal-session), [`headless`](../packages/bundle/headless), `server` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/types.ts:277`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:30`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` | -| `commands/change` | `emit` | [`packages/ui/commands/src/index.ts:172`](../packages/ui/commands/src/index.ts) | [`commands`](../packages/ui/commands) (`events.dispatch`) | `apiproxy` | +| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/index.ts:30`](../packages/interaction/user-approval/src/index.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` | +| `commands/change` | `emit` | [`packages/interaction/commands/src/index.ts:172`](../packages/interaction/commands/src/index.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` | | `credentials/updated` | `emit` | [`packages/credentials/credentials/src/index.ts:67`](../packages/credentials/credentials/src/index.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | `apiproxy`, [`credentials`](../packages/credentials/credentials) | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `apiproxy`, [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace) | | `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:64`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | @@ -29,24 +29,24 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:56`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) | | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:73`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) | -| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-title`](../packages/session-title/session-title) | -| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | -| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-title`](../packages/session-title/session-title) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`loader-smoke`](../packages/support/loader-smoke), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workspace-context`](../packages/context/workspace-context) | -| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry) | +| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | +| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) | +| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` | | `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:283`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:162`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:162`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) | | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:136`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | -| `telemetry/record` | `waterfall` | [`packages/telemetry/session-telemetry/src/index.ts:43`](../packages/telemetry/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/telemetry/session-telemetry) (`waterfall`) | - | +| `telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - | | `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:173`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | -| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:148`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`timeout-policy`](../packages/timeout/timeout-policy) | +| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:148`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`timeout-policy`](../packages/guard/timeout-policy) | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:160`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | | `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:137`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-tasks`](../packages/tasks/tool-tasks) | | `tools/result` | `emit` | [`packages/core/tools/src/index.ts:181`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) | @@ -64,7 +64,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `commands/changed` | `runtime` (`emit`) | `ui-command` | | `connection/reset` | `runtime` (`emit`) | `ui-command`, `ui-models`, `ui-permission`, `ui-settings-general` | | `credentials/changed` | `runtime` (`emit`) | `ui-models` | -| `internal/dispatch` | - | [`commands`](../packages/ui/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workflow`](../packages/workflow/workflow) | +| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `hmr`, `loader`, [`lsp-local`](../packages/lsp/lsp-local), `modules`, `webserver` | | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent) | diff --git a/docs/graph-atlas.md b/docs/graph-atlas.md index e783d5fddf..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 [core-data-structures/](core-data-structures/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/README.i18n.yaml b/docs/i18n/README.i18n.yaml index 464a3c8df0..c62de565e7 100644 --- a/docs/i18n/README.i18n.yaml +++ b/docs/i18n/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/i18n/README.md -README.md: 2bac578034441bbe786ce54c051cc622bf9275c0 -README.zh.md: b7566bf6f88e03eb06227b63011f78252b4dcd07 +README.md: 30f56eeebf15a3edec559e3080247dab9ab14178 +README.zh.md: 91d5b35a00994beea70f2bbd674cfb557d6f834d diff --git a/docs/i18n/README.md b/docs/i18n/README.md index 2bac578034..30f56eeebf 100644 --- a/docs/i18n/README.md +++ b/docs/i18n/README.md @@ -16,6 +16,8 @@ This repo's documentation is read by people and agents both inside and outside t ``` Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. `--write` stores those snapshots in the local Git object database before recording them, including uncommitted working-tree contents, and pins every distinct stored blob under a content-addressed `refs/dsh/translation-pairing/snapshots/` ref so garbage collection cannot invalidate a recorded recovery pointer. The recorded hashes therefore recover the exact last-confirmed text of either side, so an out-of-sync pair is updated by patching the counterpart minimally against the edited side's diff — never by re-translating whole files. `pnpm run gen-translation-brief ` assembles that update's working set mechanically at the narrowest safely aligned granularity — changed Markdown units, then heading sections, then whole document — with the edited side's diff since last confirmation, each changed span's three-way text, the terminology rows the change touches, and the binding update rules; a change confined to the pair's byte-identical code fences is computed outright, and `--apply` splices it into the counterpart after structural validation ([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md)). After bringing the pair back in line, `pnpm run verify-translation-pairing --write ` re-records both hashes; that yaml diff is the reviewable act of confirming consistency, which is why `--write` requires naming the pairs you confirmed (`--write --all` is the explicit corpus-wide form). + + When two branches contain valid confirmations of the same pair, the installed `dsh-translation-pairing` Git merge driver composes a new record only if Git's default text merge succeeds for both recorded owner-blob triplets and the merged pair retains its switchers and structural signature. Any uncertain shape remains an ordinary conflict; `pnpm run resolve-translation-pairing-conflicts` applies the same fail-closed operation to a merge that has already stopped, stages every safe pairing record, and exits unsuccessfully when other pairing conflicts remain. The [automatic pairing merges Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) owns the mechanism and alternatives. - **Language switcher.** Both files link to each other immediately after their H1 heading: the English file carries `English | [中文](foo.zh.md)` and the Chinese file carries `[English](foo.md) | 中文`. - **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, link targets, and verbatim code blocks match one to one across the pair — see [translation-rules.md](translation-rules.md) for the full preservation rules. Existing Markdown gates apply to `.zh.md` files unchanged (`verify-md-wrap`, `verify-md-links`). diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md index b7566bf6f8..91d5b35a00 100644 --- a/docs/i18n/README.zh.md +++ b/docs/i18n/README.zh.md @@ -16,6 +16,8 @@ ``` 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。`--write` 会先把这些快照存入本地 Git 对象库再写下记录,未提交的工作树内容也不例外;它还会在内容寻址的 `refs/dsh/translation-pairing/snapshots/` ref 下固定每个不同的已存 blob,使垃圾回收无法让已记录的恢复指针失效。因此记录的 hash 能还原任一侧上次确认时的确切文本,所以失去同步的配对是「按被改一侧的 diff 最小化地修补另一侧」,从不整篇重译。`pnpm run gen-translation-brief ` 会以能安全对齐的最窄粒度——先是有改动的 Markdown 单元,再是标题小节,最后是整篇文档——机械地汇集这次更新的工作集:被改一侧自上次确认以来的 diff、每个改动块的三方文本、改动触及的术语表行,以及有约束力的更新规则;仅落在配对中逐字节一致的围栏代码块内的改动可以直接算出,`--apply` 则经结构签名校验后把它拼接进对侧文件([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md))。两侧对齐后,`pnpm run verify-translation-pairing --write ` 重新记录两个 hash;那份 yaml diff 就是「确认一致」这个动作本身,可以被评审,也正因如此,`--write` 要求点名你确认过的配对(`--write --all` 是显式的全语料形式)。 + + 当两个分支都包含同一配对的有效确认时,已安装的 `dsh-translation-pairing` Git 合并驱动只会在 Git 默认文本合并能分别干净合并记录所指向的英文三方 blob 与中文三方 blob,且合并后的配对仍保留两侧的语言切换行和结构签名时,组合出一份新记录。任何无法确定的情形都保留为普通冲突;`pnpm run resolve-translation-pairing-conflicts` 会对已经停止的合并执行同一套遇错即保留冲突的操作,暂存每份可安全生成的配对记录,并在还有其他配对冲突时以非零状态退出。[自动配对合并 Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) 负责记录该机制与备选方案。 - **语言切换行。** 两个文件在各自 H1 标题之后立即互链:英文文件带 `English | [中文](foo.zh.md)`,中文文件带 `[English](foo.md) | 中文`。 - **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、链接目标与逐字节一致的代码块在配对两侧一一对应;完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。 diff --git a/docs/i18n/style-samples.md b/docs/i18n/style-samples.md index 87ef939730..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 [core-data-structures/](../core-data-structures/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)). -本文档描述整体行为逻辑;类型定义存放于 [core-data-structures/](../core-data-structures/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/module-graph.md b/docs/module-graph.md index 985adb16be..6f47c36e97 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -93,41 +93,22 @@ flowchart TD pkg_spill_local["spill-local"] pkg_spill_policy["spill-policy"] end - subgraph group_timeout["packages/timeout"] - pkg_timeout_policy["timeout-policy"] - end subgraph group_todo["packages/todo"] pkg_tool_todo["tool-todo"] end subgraph group_plan["packages/plan"] pkg_plan_mode["plan-mode"] end - subgraph group_cordis["packages/cordis"] - pkg_repository_plugin["repository-plugin"] - pkg_tool_cordis["tool-cordis"] - end subgraph group_hooks["packages/hooks"] pkg_hook_protocol["hook-protocol"] pkg_hooks_claude["hooks-claude"] pkg_hooks_codex["hooks-codex"] end - subgraph group_session_persistence["packages/session-persistence"] - pkg_session_checkpoint_policy["session-checkpoint-policy"] - pkg_session_persistence["session-persistence"] - pkg_session_persistence_jsonl["session-persistence-jsonl"] - pkg_session_persistence_sqlite["session-persistence-sqlite"] - end subgraph group_session_query["packages/session-query"] pkg_session_query["session-query"] pkg_session_query_sqlite["session-query-sqlite"] pkg_tool_session_query["tool-session-query"] end - subgraph group_session_title["packages/session-title"] - pkg_session_title["session-title"] - pkg_session_title_all_messages_llm["session-title-all-messages-llm"] - pkg_session_title_first_message_llm["session-title-first-message-llm"] - pkg_session_title_llm["session-title-llm"] - end subgraph group_support["packages/support"] pkg_acp_snapshot["acp-snapshot"] pkg_agent_loop_testkit["agent-loop-testkit"] @@ -139,19 +120,13 @@ flowchart TD subgraph group_acp["packages/acp"] pkg_acp["acp"] end - subgraph group_ui["packages/ui"] - pkg_app_boot["app-boot"] - pkg_commands["commands"] - pkg_jsonrpc["jsonrpc"] - pkg_permission["permission"] - pkg_tool_ask_user["tool-ask-user"] - pkg_user_approval["user-approval"] - pkg_user_interaction["user-interaction"] - end subgraph group_api["packages/api"] pkg_api_gateway["api-gateway"] pkg_api_remotes["api-remotes"] end + subgraph group_boot["packages/boot"] + pkg_app_boot["app-boot"] + end subgraph group_bundle["packages/bundle"] pkg_base["base"] pkg_headless["headless"] @@ -165,7 +140,6 @@ flowchart TD pkg_client_runtime["client-runtime"] pkg_client_schema_form["client-schema-form"] pkg_client_test_runtime["client-test-runtime"] - pkg_client_ui_agent_preset["client-ui-agent-preset"] pkg_client_ui_command["client-ui-command"] pkg_client_ui_conversation["client-ui-conversation"] pkg_client_ui_deliverables["client-ui-deliverables"] @@ -220,6 +194,7 @@ flowchart TD end subgraph group_guard["packages/guard"] pkg_repeat_tool_guard["repeat-tool-guard"] + pkg_timeout_policy["timeout-policy"] end subgraph group_host["packages/host"] pkg_frontend_static["frontend-static"] @@ -230,6 +205,13 @@ flowchart TD pkg_host_directory_picker_native["host-directory-picker-native"] pkg_host_webserver["host-webserver"] end + subgraph group_interaction["packages/interaction"] + pkg_commands["commands"] + pkg_permission["permission"] + pkg_tool_ask_user["tool-ask-user"] + pkg_user_approval["user-approval"] + pkg_user_interaction["user-interaction"] + end subgraph group_lsp["packages/lsp"] pkg_lsp["lsp"] pkg_lsp_local["lsp-local"] @@ -238,10 +220,6 @@ flowchart TD subgraph group_mcp["packages/mcp"] pkg_mcp_client["mcp-client"] end - subgraph group_preset["packages/preset"] - pkg_agent_presets["agent-presets"] - pkg_persona["persona"] - end subgraph group_pty["packages/pty"] pkg_pty["pty"] pkg_pty_local["pty-local"] @@ -253,16 +231,31 @@ flowchart TD pkg_sandbox_local["sandbox-local"] pkg_sandbox_policy["sandbox-policy"] end - subgraph group_sdk["packages/sdk"] + subgraph group_scaffold["packages/scaffold"] pkg_helper["helper"] + pkg_jsonrpc["jsonrpc"] pkg_scripts["scripts"] pkg_sdk_client["sdk-client"] pkg_sdk_protocol["sdk-protocol"] pkg_telemetry["telemetry"] end - subgraph group_session_projection["packages/session-projection"] + subgraph group_self_modification["packages/self-modification"] + pkg_repository_plugin["repository-plugin"] + pkg_tool_cordis["tool-cordis"] + end + subgraph group_session["packages/session"] + pkg_session_checkpoint_policy["session-checkpoint-policy"] + pkg_session_persistence["session-persistence"] + pkg_session_persistence_jsonl["session-persistence-jsonl"] + pkg_session_persistence_sqlite["session-persistence-sqlite"] pkg_session_projection["session-projection"] pkg_session_projection_cache["session-projection-cache"] + pkg_session_telemetry["session-telemetry"] + pkg_session_telemetry_otel["session-telemetry-otel"] + pkg_session_title["session-title"] + pkg_session_title_all_messages_llm["session-title-all-messages-llm"] + pkg_session_title_first_message_llm["session-title-first-message-llm"] + pkg_session_title_llm["session-title-llm"] end subgraph group_settings["packages/settings"] pkg_settings["settings"] @@ -283,10 +276,6 @@ flowchart TD pkg_tasks_local["tasks-local"] pkg_tool_tasks["tool-tasks"] end - subgraph group_telemetry["packages/telemetry"] - pkg_session_telemetry["session-telemetry"] - pkg_session_telemetry_otel["session-telemetry-otel"] - end subgraph group_typert["packages/typert"] pkg_type_meta["type-meta"] pkg_typert_generator["typert-generator"] @@ -321,6 +310,7 @@ flowchart TD pkg_code_runtime --> pkg_invariants pkg_e2b --> pkg_invariants pkg_jsonrpc_demo --> pkg_invariants + pkg_host_apiproxy --> pkg_invariants pkg_host_directory_picker --> pkg_invariants pkg_host_webserver --> pkg_invariants pkg_storage --> pkg_invariants @@ -397,6 +387,11 @@ flowchart TD pkg_client_locale --> pkg_client_ui_primitives pkg_client_locale --> pkg_client_ui_slots pkg_client_locale --> pkg_invariants + pkg_client_test_runtime --> pkg_client_runtime + pkg_client_test_runtime --> pkg_client_ui_slots + pkg_client_test_runtime --> pkg_client_web_react + pkg_client_test_runtime --> pkg_host_apiproxy + pkg_client_test_runtime --> pkg_invariants pkg_client_ui_models --> pkg_client_connection pkg_client_ui_models --> pkg_client_runtime pkg_client_ui_models --> pkg_client_schema_form @@ -456,10 +451,6 @@ flowchart TD pkg_spill --> pkg_invariants pkg_spill --> pkg_llm pkg_spill --> pkg_session - pkg_session_persistence --> pkg_brand - pkg_session_persistence --> pkg_invariants - pkg_session_persistence --> pkg_session - pkg_session_persistence --> pkg_timeout pkg_acp_snapshot --> pkg_invariants pkg_acp_snapshot --> pkg_session pkg_app_boot --> pkg_environment @@ -500,16 +491,13 @@ flowchart TD pkg_code_runtime_worker --> pkg_invariants pkg_code_runtime_worker --> pkg_session pkg_code_runtime_worker --> pkg_timeout - pkg_agent_presets --> pkg_invariants - pkg_agent_presets --> pkg_paths - pkg_agent_presets --> pkg_scope - pkg_agent_presets --> pkg_session - pkg_agent_presets --> pkg_settings - pkg_persona --> pkg_invariants - pkg_persona --> pkg_system_prompt pkg_sandbox_local --> pkg_invariants pkg_sandbox_local --> pkg_llm pkg_sandbox_local --> pkg_sandbox + pkg_session_persistence --> pkg_brand + pkg_session_persistence --> pkg_invariants + pkg_session_persistence --> pkg_session + pkg_session_persistence --> pkg_timeout pkg_session_projection --> pkg_invariants pkg_session_projection --> pkg_session pkg_llm_retry --> pkg_agent @@ -557,17 +545,6 @@ flowchart TD pkg_hook_protocol --> pkg_bash pkg_hook_protocol --> pkg_invariants pkg_hook_protocol --> pkg_session - pkg_session_persistence_jsonl --> pkg_invariants - pkg_session_persistence_jsonl --> pkg_session - pkg_session_persistence_jsonl --> pkg_session_persistence - pkg_session_persistence_sqlite --> pkg_invariants - pkg_session_persistence_sqlite --> pkg_session - pkg_session_persistence_sqlite --> pkg_session_persistence - pkg_session_title --> pkg_brand - pkg_session_title --> pkg_invariants - pkg_session_title --> pkg_llm - pkg_session_title --> pkg_session - pkg_session_title --> pkg_session_projection pkg_llm_replay --> pkg_compact pkg_llm_replay --> pkg_invariants pkg_llm_replay --> pkg_llm @@ -576,6 +553,35 @@ flowchart TD pkg_loader_smoke --> pkg_invariants pkg_loader_smoke --> pkg_llm pkg_loader_smoke --> pkg_session + pkg_headless --> pkg_agent + pkg_headless --> pkg_host_apiproxy + pkg_headless --> pkg_host_webserver + pkg_headless --> pkg_invariants + pkg_headless --> pkg_session + pkg_client_ui_layout --> pkg_client_runtime + pkg_client_ui_layout --> pkg_client_ui_slots + pkg_client_ui_layout --> pkg_client_ui_theme + pkg_client_ui_layout --> pkg_invariants + pkg_time_context --> pkg_agent + pkg_time_context --> pkg_invariants + pkg_time_context --> pkg_session + pkg_tmux_context --> pkg_agent + pkg_tmux_context --> pkg_bash + pkg_tmux_context --> pkg_invariants + pkg_tmux_context --> pkg_session + pkg_fs_e2b --> pkg_e2b + pkg_fs_e2b --> pkg_fs + pkg_fs_e2b --> pkg_invariants + pkg_host_directory_picker_browse --> pkg_client_locale + pkg_host_directory_picker_browse --> pkg_client_runtime + pkg_host_directory_picker_browse --> pkg_client_ui_primitives + pkg_host_directory_picker_browse --> pkg_client_ui_slots + pkg_host_directory_picker_browse --> pkg_client_ui_workspace + pkg_host_directory_picker_browse --> pkg_invariants + pkg_host_directory_picker_native --> pkg_client_runtime + pkg_host_directory_picker_native --> pkg_client_ui_slots + pkg_host_directory_picker_native --> pkg_client_ui_workspace + pkg_host_directory_picker_native --> pkg_invariants pkg_commands --> pkg_agent pkg_commands --> pkg_brand pkg_commands --> pkg_invariants @@ -591,32 +597,6 @@ flowchart TD pkg_user_interaction --> pkg_agent pkg_user_interaction --> pkg_invariants pkg_user_interaction --> pkg_llm - pkg_client_ui_layout --> pkg_client_runtime - pkg_client_ui_layout --> pkg_client_ui_slots - pkg_client_ui_layout --> pkg_client_ui_theme - pkg_client_ui_layout --> pkg_invariants - pkg_time_context --> pkg_agent - pkg_time_context --> pkg_invariants - pkg_time_context --> pkg_session - pkg_tmux_context --> pkg_agent - pkg_tmux_context --> pkg_bash - pkg_tmux_context --> pkg_invariants - pkg_tmux_context --> pkg_session - pkg_fs_e2b --> pkg_e2b - pkg_fs_e2b --> pkg_fs - pkg_fs_e2b --> pkg_invariants - pkg_host_apiproxy --> pkg_agent_presets - pkg_host_apiproxy --> pkg_invariants - pkg_host_directory_picker_browse --> pkg_client_locale - pkg_host_directory_picker_browse --> pkg_client_runtime - pkg_host_directory_picker_browse --> pkg_client_ui_primitives - pkg_host_directory_picker_browse --> pkg_client_ui_slots - pkg_host_directory_picker_browse --> pkg_client_ui_workspace - pkg_host_directory_picker_browse --> pkg_invariants - pkg_host_directory_picker_native --> pkg_client_runtime - pkg_host_directory_picker_native --> pkg_client_ui_slots - pkg_host_directory_picker_native --> pkg_client_ui_workspace - pkg_host_directory_picker_native --> pkg_invariants pkg_lsp_local --> pkg_brand pkg_lsp_local --> pkg_fs pkg_lsp_local --> pkg_invariants @@ -634,18 +614,29 @@ flowchart TD pkg_sandbox_policy --> pkg_system_prompt pkg_scripts --> pkg_app_boot pkg_scripts --> pkg_invariants + pkg_session_persistence_jsonl --> pkg_invariants + pkg_session_persistence_jsonl --> pkg_session + pkg_session_persistence_jsonl --> pkg_session_persistence + pkg_session_persistence_sqlite --> pkg_invariants + pkg_session_persistence_sqlite --> pkg_session + pkg_session_persistence_sqlite --> pkg_session_persistence pkg_session_projection_cache --> pkg_invariants pkg_session_projection_cache --> pkg_session pkg_session_projection_cache --> pkg_session_persistence pkg_session_projection_cache --> pkg_session_projection pkg_session_projection_cache --> pkg_storage_domain + pkg_session_telemetry --> pkg_agent + pkg_session_telemetry --> pkg_invariants + pkg_session_telemetry --> pkg_session + pkg_session_title --> pkg_brand + pkg_session_title --> pkg_invariants + pkg_session_title --> pkg_llm + pkg_session_title --> pkg_session + pkg_session_title --> pkg_session_projection pkg_tasks --> pkg_agent pkg_tasks --> pkg_brand pkg_tasks --> pkg_invariants pkg_tasks --> pkg_session - pkg_session_telemetry --> pkg_agent - pkg_session_telemetry --> pkg_invariants - pkg_session_telemetry --> pkg_session pkg_workflow --> pkg_agent pkg_workflow --> pkg_brand pkg_workflow --> pkg_invariants @@ -697,40 +688,16 @@ flowchart TD pkg_session_query --> pkg_session pkg_session_query --> pkg_session_persistence pkg_session_query --> pkg_session_title - pkg_session_title_llm --> pkg_invariants - pkg_session_title_llm --> pkg_llm - pkg_session_title_llm --> pkg_session - pkg_session_title_llm --> pkg_session_title - pkg_session_title_llm --> pkg_timeout pkg_acp --> pkg_agent pkg_acp --> pkg_invariants pkg_acp --> pkg_session pkg_acp --> pkg_user_approval - pkg_permission --> pkg_bash - pkg_permission --> pkg_commands - pkg_permission --> pkg_invariants - pkg_permission --> pkg_sandbox - pkg_permission --> pkg_sandbox_policy - pkg_permission --> pkg_session - pkg_permission --> pkg_session_projection - pkg_permission --> pkg_settings - pkg_permission --> pkg_user_approval pkg_api_remotes --> pkg_agent pkg_api_remotes --> pkg_goal pkg_api_remotes --> pkg_invariants pkg_api_remotes --> pkg_session pkg_api_remotes --> pkg_session_persistence pkg_api_remotes --> pkg_typert_registry - pkg_headless --> pkg_agent - pkg_headless --> pkg_host_apiproxy - pkg_headless --> pkg_host_webserver - pkg_headless --> pkg_invariants - pkg_headless --> pkg_session - pkg_client_test_runtime --> pkg_client_runtime - pkg_client_test_runtime --> pkg_client_ui_slots - pkg_client_test_runtime --> pkg_client_web_react - pkg_client_test_runtime --> pkg_host_apiproxy - pkg_client_test_runtime --> pkg_invariants pkg_client_ui_conversation --> pkg_client_locale pkg_client_ui_conversation --> pkg_client_runtime pkg_client_ui_conversation --> pkg_client_ui_primitives @@ -745,6 +712,15 @@ flowchart TD pkg_host_directory_picker_auto --> pkg_host_directory_picker_native pkg_host_directory_picker_auto --> pkg_host_webserver pkg_host_directory_picker_auto --> pkg_invariants + pkg_permission --> pkg_bash + pkg_permission --> pkg_commands + pkg_permission --> pkg_invariants + pkg_permission --> pkg_sandbox + pkg_permission --> pkg_sandbox_policy + pkg_permission --> pkg_session + pkg_permission --> pkg_session_projection + pkg_permission --> pkg_settings + pkg_permission --> pkg_user_approval pkg_pty_local --> pkg_agent pkg_pty_local --> pkg_invariants pkg_pty_local --> pkg_pty @@ -752,6 +728,11 @@ flowchart TD pkg_pty_local --> pkg_sandbox_policy pkg_pty_local --> pkg_session pkg_pty_local --> pkg_subprocess + pkg_session_title_llm --> pkg_invariants + pkg_session_title_llm --> pkg_llm + pkg_session_title_llm --> pkg_session + pkg_session_title_llm --> pkg_session_title + pkg_session_title_llm --> pkg_timeout pkg_tasks_local --> pkg_agent pkg_tasks_local --> pkg_invariants pkg_tasks_local --> pkg_tasks @@ -833,10 +814,6 @@ flowchart TD pkg_spill_policy --> pkg_session pkg_spill_policy --> pkg_spill pkg_spill_policy --> pkg_tools - pkg_timeout_policy --> pkg_invariants - pkg_timeout_policy --> pkg_llm - pkg_timeout_policy --> pkg_timeout - pkg_timeout_policy --> pkg_tools pkg_tool_todo --> pkg_agent pkg_tool_todo --> pkg_invariants pkg_tool_todo --> pkg_session @@ -851,9 +828,6 @@ flowchart TD pkg_plan_mode --> pkg_system_prompt pkg_plan_mode --> pkg_tools pkg_plan_mode --> pkg_user_interaction - pkg_tool_cordis --> pkg_invariants - pkg_tool_cordis --> pkg_scope - pkg_tool_cordis --> pkg_tools pkg_hooks_codex --> pkg_agent pkg_hooks_codex --> pkg_hook_protocol pkg_hooks_codex --> pkg_invariants @@ -861,12 +835,6 @@ flowchart TD pkg_hooks_codex --> pkg_session pkg_hooks_codex --> pkg_session_persistence pkg_hooks_codex --> pkg_tools - pkg_session_checkpoint_policy --> pkg_agent - pkg_session_checkpoint_policy --> pkg_invariants - pkg_session_checkpoint_policy --> pkg_llm - pkg_session_checkpoint_policy --> pkg_session - pkg_session_checkpoint_policy --> pkg_session_persistence - pkg_session_checkpoint_policy --> pkg_tools pkg_session_query_sqlite --> pkg_invariants pkg_session_query_sqlite --> pkg_session pkg_session_query_sqlite --> pkg_session_persistence @@ -878,33 +846,12 @@ flowchart TD pkg_tool_session_query --> pkg_system_prompt pkg_tool_session_query --> pkg_timeout pkg_tool_session_query --> pkg_tools - pkg_session_title_all_messages_llm --> pkg_invariants - pkg_session_title_all_messages_llm --> pkg_llm - pkg_session_title_all_messages_llm --> pkg_session - pkg_session_title_all_messages_llm --> pkg_session_title - pkg_session_title_all_messages_llm --> pkg_session_title_llm - pkg_session_title_first_message_llm --> pkg_invariants - pkg_session_title_first_message_llm --> pkg_llm - pkg_session_title_first_message_llm --> pkg_session - pkg_session_title_first_message_llm --> pkg_session_title - pkg_session_title_first_message_llm --> pkg_session_title_llm pkg_agent_loop_testkit --> pkg_agent pkg_agent_loop_testkit --> pkg_invariants pkg_agent_loop_testkit --> pkg_llm pkg_agent_loop_testkit --> pkg_session pkg_agent_loop_testkit --> pkg_system_prompt pkg_agent_loop_testkit --> pkg_tools - pkg_tool_ask_user --> pkg_agent - pkg_tool_ask_user --> pkg_invariants - pkg_tool_ask_user --> pkg_tools - pkg_tool_ask_user --> pkg_user_interaction - pkg_client_ui_agent_preset --> pkg_client_connection - pkg_client_ui_agent_preset --> pkg_client_locale - pkg_client_ui_agent_preset --> pkg_client_runtime - pkg_client_ui_agent_preset --> pkg_client_ui_conversation - pkg_client_ui_agent_preset --> pkg_client_ui_primitives - pkg_client_ui_agent_preset --> pkg_client_ui_slots - pkg_client_ui_agent_preset --> pkg_invariants pkg_client_ui_command --> pkg_client_connection pkg_client_ui_command --> pkg_client_locale pkg_client_ui_command --> pkg_client_runtime @@ -949,6 +896,14 @@ flowchart TD pkg_repeat_tool_guard --> pkg_agent pkg_repeat_tool_guard --> pkg_invariants pkg_repeat_tool_guard --> pkg_tools + pkg_timeout_policy --> pkg_invariants + pkg_timeout_policy --> pkg_llm + pkg_timeout_policy --> pkg_timeout + pkg_timeout_policy --> pkg_tools + pkg_tool_ask_user --> pkg_agent + pkg_tool_ask_user --> pkg_invariants + pkg_tool_ask_user --> pkg_tools + pkg_tool_ask_user --> pkg_user_interaction pkg_tool_lsp --> pkg_invariants pkg_tool_lsp --> pkg_llm pkg_tool_lsp --> pkg_lsp @@ -972,13 +927,15 @@ flowchart TD pkg_tool_pty --> pkg_system_prompt pkg_tool_pty --> pkg_tasks pkg_tool_pty --> pkg_tools - pkg_tool_tasks --> pkg_agent - pkg_tool_tasks --> pkg_invariants - pkg_tool_tasks --> pkg_llm - pkg_tool_tasks --> pkg_retention - pkg_tool_tasks --> pkg_system_prompt - pkg_tool_tasks --> pkg_tasks - pkg_tool_tasks --> pkg_tools + pkg_tool_cordis --> pkg_invariants + pkg_tool_cordis --> pkg_scope + pkg_tool_cordis --> pkg_tools + pkg_session_checkpoint_policy --> pkg_agent + pkg_session_checkpoint_policy --> pkg_invariants + pkg_session_checkpoint_policy --> pkg_llm + pkg_session_checkpoint_policy --> pkg_session + pkg_session_checkpoint_policy --> pkg_session_persistence + pkg_session_checkpoint_policy --> pkg_tools pkg_session_telemetry_otel --> pkg_brand pkg_session_telemetry_otel --> pkg_command_feedback pkg_session_telemetry_otel --> pkg_invariants @@ -986,6 +943,23 @@ flowchart TD pkg_session_telemetry_otel --> pkg_paths pkg_session_telemetry_otel --> pkg_session pkg_session_telemetry_otel --> pkg_session_telemetry + pkg_session_title_all_messages_llm --> pkg_invariants + pkg_session_title_all_messages_llm --> pkg_llm + pkg_session_title_all_messages_llm --> pkg_session + pkg_session_title_all_messages_llm --> pkg_session_title + pkg_session_title_all_messages_llm --> pkg_session_title_llm + pkg_session_title_first_message_llm --> pkg_invariants + pkg_session_title_first_message_llm --> pkg_llm + pkg_session_title_first_message_llm --> pkg_session + pkg_session_title_first_message_llm --> pkg_session_title + pkg_session_title_first_message_llm --> pkg_session_title_llm + pkg_tool_tasks --> pkg_agent + pkg_tool_tasks --> pkg_invariants + pkg_tool_tasks --> pkg_llm + pkg_tool_tasks --> pkg_retention + pkg_tool_tasks --> pkg_system_prompt + pkg_tool_tasks --> pkg_tasks + pkg_tool_tasks --> pkg_tools pkg_tool_workflow --> pkg_agent pkg_tool_workflow --> pkg_invariants pkg_tool_workflow --> pkg_llm @@ -1048,10 +1022,6 @@ flowchart TD pkg_tool_subagent_report --> pkg_llm pkg_tool_subagent_report --> pkg_subagent pkg_tool_subagent_report --> pkg_tools - pkg_repository_plugin --> pkg_invariants - pkg_repository_plugin --> pkg_mcp_client - pkg_repository_plugin --> pkg_paths - pkg_repository_plugin --> pkg_skill_local pkg_hooks_claude --> pkg_agent pkg_hooks_claude --> pkg_hook_protocol pkg_hooks_claude --> pkg_invariants @@ -1111,6 +1081,10 @@ flowchart TD pkg_sdk_protocol --> pkg_llm pkg_sdk_protocol --> pkg_session pkg_sdk_protocol --> pkg_subagent + pkg_repository_plugin --> pkg_invariants + pkg_repository_plugin --> pkg_mcp_client + pkg_repository_plugin --> pkg_paths + pkg_repository_plugin --> pkg_skill_local pkg_tool_ralph --> pkg_agent pkg_tool_ralph --> pkg_invariants pkg_tool_ralph --> pkg_llm @@ -1141,14 +1115,6 @@ flowchart TD pkg_subagent_spawn --> pkg_invariants pkg_subagent_spawn --> pkg_subagent pkg_subagent_spawn --> pkg_subagent_inprocess - pkg_jsonrpc --> pkg_agent - pkg_jsonrpc --> pkg_invariants - pkg_jsonrpc --> pkg_llm - pkg_jsonrpc --> pkg_llm_deepseek - pkg_jsonrpc --> pkg_scope - pkg_jsonrpc --> pkg_sdk_protocol - pkg_jsonrpc --> pkg_session - pkg_jsonrpc --> pkg_subagent pkg_agent_spine_demo --> pkg_agent pkg_agent_spine_demo --> pkg_agent_loop pkg_agent_spine_demo --> pkg_bash_env @@ -1171,6 +1137,14 @@ flowchart TD pkg_agent_spine_demo --> pkg_tool_tasks pkg_agent_spine_demo --> pkg_tools pkg_agent_spine_demo --> pkg_workspace_context + pkg_jsonrpc --> pkg_agent + pkg_jsonrpc --> pkg_invariants + pkg_jsonrpc --> pkg_llm + pkg_jsonrpc --> pkg_llm_deepseek + pkg_jsonrpc --> pkg_scope + pkg_jsonrpc --> pkg_sdk_protocol + pkg_jsonrpc --> pkg_session + pkg_jsonrpc --> pkg_subagent pkg_sdk_client --> pkg_invariants pkg_sdk_client --> pkg_llm pkg_sdk_client --> pkg_sdk_protocol @@ -1216,6 +1190,7 @@ flowchart TD | [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | [`invariants`](../packages/support/invariants) | | [`e2b`](../packages/e2b/e2b) | `e2b` | [`invariants`](../packages/support/invariants) | | [`jsonrpc-demo`](../packages/examples/jsonrpc-demo) | `examples` | [`invariants`](../packages/support/invariants) | +| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`invariants`](../packages/support/invariants) | | [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/support/invariants) | | [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/support/invariants) | | [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/support/invariants) | @@ -1230,8 +1205,8 @@ flowchart TD | [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | -| [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) | -| [`telemetry`](../packages/sdk/telemetry) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | +| [`helper`](../packages/scaffold/helper) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) | +| [`telemetry`](../packages/scaffold/telemetry) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) | | [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) | @@ -1246,6 +1221,7 @@ flowchart TD | [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | | [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) | | [`client-locale`](../packages/client/locale) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) | | [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | | [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) | @@ -1262,9 +1238,8 @@ flowchart TD | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) | | [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) | | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`session-persistence`](../packages/session-persistence/session-persistence) | `session-persistence` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | | [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | -| [`app-boot`](../packages/ui/app-boot) | `ui` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) | +| [`app-boot`](../packages/boot/app-boot) | `boot` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) | | [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) | | [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | @@ -1272,13 +1247,12 @@ flowchart TD | [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | -| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) | -| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | -| [`session-projection`](../packages/session-projection/session-projection) | `session-projection` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | +| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | +| [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | -| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection) | -| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection), [`type-meta`](../packages/typert/type-meta) | +| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | +| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`type-meta`](../packages/typert/type-meta) | | [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`pwsh-local`](../packages/bash/pwsh-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | @@ -1287,73 +1261,64 @@ flowchart TD | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`web`](../packages/web/web) | | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) | | [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | -| [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | -| [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | -| [`session-title`](../packages/session-title/session-title) | `session-title` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection) | | [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`commands`](../packages/ui/commands) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) | -| [`user-approval`](../packages/ui/user-approval) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | -| [`user-interaction`](../packages/ui/user-interaction) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | +| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | -| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) | | [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | +| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) | +| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | +| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | | [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`pty`](../packages/pty/pty) | `pty` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | -| [`scripts`](../packages/sdk/scripts) | `sdk` | [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants) | -| [`session-projection-cache`](../packages/session-projection/session-projection-cache) | `session-projection` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`storage-domain`](../packages/storage/storage-domain) | +| [`scripts`](../packages/scaffold/scripts) | `scaffold` | [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants) | +| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | +| [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | +| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) | +| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | +| [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`tasks`](../packages/tasks/tasks) | `tasks` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | -| [`session-telemetry`](../packages/telemetry/session-telemetry) | `telemetry` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) | -| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) | -| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/ui/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) | +| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) | +| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) | +| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) | | [`goal-session`](../packages/goal/goal-session) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | | [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | -| [`command-compact`](../packages/compact/command-compact) | `compact` | [`commands`](../packages/ui/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants) | +| [`command-compact`](../packages/compact/command-compact) | `compact` | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants) | | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | -| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-title`](../packages/session-title/session-title) | -| [`session-title-llm`](../packages/session-title/session-title-llm) | `session-title` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`timeout`](../packages/util/timeout) | -| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) | -| [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`commands`](../packages/ui/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/ui/user-approval) | -| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`typert-registry`](../packages/typert/registry) | -| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | -| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) | +| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) | +| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) | +| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-registry`](../packages/typert/registry) | | [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`token-meter`](../packages/llm/token-meter) | -| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/ui/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | +| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | +| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) | | [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) | +| [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) | | [`tasks-local`](../packages/tasks/tasks-local) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tasks`](../packages/tasks/tasks), [`timeout`](../packages/util/timeout) | -| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`bash-env`](../packages/bash/bash-env) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools) | -| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | +| [`bash-env`](../packages/bash/bash-env) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | +| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | -| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | +| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | -| [`timeout-policy`](../packages/timeout/timeout-policy) | `timeout` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | -| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection), [`tools`](../packages/core/tools) | -| [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/ui/commands), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | -| [`tool-cordis`](../packages/cordis/tool-cordis) | `cordis` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) | -| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools) | -| [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy) | `session-persistence` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools) | -| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query) | +| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | +| [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-interaction`](../packages/interaction/user-interaction) | +| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | +| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) | | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | -| [`session-title-all-messages-llm`](../packages/session-title/session-title-all-messages-llm) | `session-title` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`session-title-llm`](../packages/session-title/session-title-llm) | -| [`session-title-first-message-llm`](../packages/session-title/session-title-first-message-llm) | `session-title` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`session-title-llm`](../packages/session-title/session-title-llm) | | [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | -| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) | @@ -1361,37 +1326,43 @@ flowchart TD | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) | | [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | +| [`timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-ask-user`](../packages/interaction/tool-ask-user) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/interaction/user-interaction) | | [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) | | [`tool-bash-persistent`](../packages/pty/tool-bash-persistent) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`tool-pty`](../packages/pty/tool-pty) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`pty`](../packages/pty/pty), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | +| [`tool-cordis`](../packages/self-modification/tool-cordis) | `self-modification` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) | +| [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | +| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | +| [`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | +| [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | | [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | -| [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel) | `telemetry` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`session-telemetry`](../packages/telemetry/session-telemetry) | | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | -| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | +| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-pwsh`](../packages/bash/tool-pwsh) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | -| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | +| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`repository-plugin`](../packages/cordis/repository-plugin) | `cordis` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) | -| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | +| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) | | [`client-ui-model`](../packages/client/ui-model) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-command`](../packages/client/ui-command), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/ui/permission) | +| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/interaction/permission) | | [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) | | [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/support/invariants) | | [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) | -| [`sdk-protocol`](../packages/sdk/sdk-protocol) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | -| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/sdk-protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | +| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | -| [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/sdk-protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | -| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | -| [`sdk-client`](../packages/sdk/sdk-client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/sdk-protocol), [`session`](../packages/core/session) | -| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/sdk-client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | -| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | +| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | +| [`jsonrpc`](../packages/scaffold/server) | `scaffold` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`sdk-client`](../packages/scaffold/client) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session) | +| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/scaffold/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | +| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index cf7e517af3..ad021fc1ac 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -3,11 +3,11 @@ # 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](core-data-structures/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](core-data-structures/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). -The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](core-data-structures/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction. +The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction. ## Event envelope @@ -139,9 +139,9 @@ Source: [`packages/preset/agent-presets/src/session.ts:26`](../packages/preset/a } ``` -Types: [CallId](core-data-structures/core.md) +Types: [CallId](subsystems/core.md) -Source: [`packages/ui/user-approval/src/index.ts:44`](../packages/ui/user-approval/src/index.ts) +Source: [`packages/interaction/user-approval/src/index.ts:44`](../packages/interaction/user-approval/src/index.ts) #### `approval/decided` — log-only @@ -157,7 +157,7 @@ Source: [`packages/ui/user-approval/src/index.ts:44`](../packages/ui/user-approv } ``` -Source: [`packages/ui/user-approval/src/index.ts:55`](../packages/ui/user-approval/src/index.ts) +Source: [`packages/interaction/user-approval/src/index.ts:55`](../packages/interaction/user-approval/src/index.ts) #### `approval/policy` — log-only @@ -177,7 +177,7 @@ Source: [`packages/ui/user-approval/src/index.ts:55`](../packages/ui/user-approv } ``` -Source: [`packages/ui/user-approval/src/index.ts:67`](../packages/ui/user-approval/src/index.ts) +Source: [`packages/interaction/user-approval/src/index.ts:67`](../packages/interaction/user-approval/src/index.ts) ### `assistant/*` @@ -188,7 +188,7 @@ Source: [`packages/ui/user-approval/src/index.ts:67`](../packages/ui/user-approv 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk } ``` -Types: [StreamChunk](core-data-structures/llm-streaming.md) +Types: [StreamChunk](subsystems/llm-streaming.md) Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts) @@ -204,7 +204,7 @@ Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/ 'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage } ``` -Types: [TokenUsage](core-data-structures/llm-streaming.md) +Types: [TokenUsage](subsystems/llm-streaming.md) Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/types.ts) @@ -227,7 +227,7 @@ Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/ } ``` -Source: [`packages/ui/commands/src/index.ts:151`](../packages/ui/commands/src/index.ts) +Source: [`packages/interaction/commands/src/index.ts:151`](../packages/interaction/commands/src/index.ts) #### `command/run` — log-only @@ -245,7 +245,7 @@ Source: [`packages/ui/commands/src/index.ts:151`](../packages/ui/commands/src/in 'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource } ``` -Source: [`packages/ui/commands/src/index.ts:144`](../packages/ui/commands/src/index.ts) +Source: [`packages/interaction/commands/src/index.ts:144`](../packages/interaction/commands/src/index.ts) ### `compact/*` @@ -344,7 +344,7 @@ Source: [`packages/compact/compact/src/types.ts:19`](../packages/compact/compact ) ``` -Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md) +Types: [ContentBlock](subsystems/core.md) · [TokenUsage](subsystems/llm-streaming.md) Source: [`packages/compact/compact/src/types.ts:29`](../packages/compact/compact/src/types.ts) @@ -465,7 +465,7 @@ Source: [`packages/llm/llm-retry/src/index.ts:17`](../packages/llm/llm-retry/src 'permission/preset': { preset: string } ``` -Source: [`packages/ui/permission/src/index.ts:50`](../packages/ui/permission/src/index.ts) +Source: [`packages/interaction/permission/src/index.ts:50`](../packages/interaction/permission/src/index.ts) ### `plan/*` @@ -571,9 +571,9 @@ Source: [`packages/core/session/src/types.ts:312`](../packages/core/session/src/ 'session/title': SessionTitleEventData ``` -Types: [SessionTitleEventData](core-data-structures/session-title.md) +Types: [SessionTitleEventData](subsystems/session-title.md) -Source: [`packages/session-title/session-title/src/index.ts:100`](../packages/session-title/session-title/src/index.ts) +Source: [`packages/session/session-title/src/index.ts:100`](../packages/session/session-title/src/index.ts) #### `session/title-llm-request` — log-only @@ -582,9 +582,9 @@ Source: [`packages/session-title/session-title/src/index.ts:100`](../packages/se 'session/title-llm-request': SessionTitleLlmRequestEventData ``` -Types: [SessionTitleLlmRequestEventData](core-data-structures/session-title.md) +Types: [SessionTitleLlmRequestEventData](subsystems/session-title.md) -Source: [`packages/session-title/session-title-llm/src/index.ts:43`](../packages/session-title/session-title-llm/src/index.ts) +Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts) ### `step/*` @@ -632,7 +632,7 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent 'todo/write': { todos: TodoItem[] } ``` -Types: [TodoItem](core-data-structures/session.md) +Types: [TodoItem](subsystems/session.md) Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts) @@ -649,7 +649,7 @@ Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/ 'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string } ``` -Types: [CallId](core-data-structures/core.md) +Types: [CallId](subsystems/core.md) Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts) @@ -674,7 +674,7 @@ Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/ 'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; content: ContentBlock[] } ``` -Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md) +Types: [CallId](subsystems/core.md) · [ContentBlock](subsystems/core.md) Source: [`packages/core/tools/src/code-mode.ts:49`](../packages/core/tools/src/code-mode.ts) @@ -697,7 +697,7 @@ Source: [`packages/core/tools/src/code-mode.ts:49`](../packages/core/tools/src/c 'tool/code-dispatch-start': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown } ``` -Types: [CallId](core-data-structures/core.md) +Types: [CallId](subsystems/core.md) Source: [`packages/core/tools/src/code-mode.ts:33`](../packages/core/tools/src/code-mode.ts) @@ -742,7 +742,7 @@ Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/ 'turn/end': { turn: number; reason: TurnEndReason } ``` -Types: [TurnEndReason](core-data-structures/session.md) +Types: [TurnEndReason](subsystems/session.md) Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts) diff --git a/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.i18n.yaml b/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.i18n.yaml index 47a2e3fdd7..b54453f3c4 100644 --- a/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.i18n.yaml +++ b/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.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/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md -0004-landlock-partial-notice-misclassified-child-failures.md: db810fdc896f9734d1b581617838d72166f4efc9 -0004-landlock-partial-notice-misclassified-child-failures.zh.md: 4a31fb038c44b036e6a183040947295a47221967 +0004-landlock-partial-notice-misclassified-child-failures.md: ed59fa9d2b04623220a9bcd984fe6becdc619198 +0004-landlock-partial-notice-misclassified-child-failures.zh.md: 5cf3e4422a5b888553e12dfb97b8a26aad65e231 diff --git a/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md b/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md index db810fdc89..ed59fa9d2b 100644 --- a/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md +++ b/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md @@ -40,7 +40,7 @@ Stderr remains an in-band attribution channel. A confined child can deliberately ## Guardrails added -- [`RunnerFailureRule`](../core-data-structures/sandbox.md#wrapped-argv-and-classification-dialects) carries optional allowed exit codes, case-insensitive per-line fatal signatures, and case-insensitive exact informational-line exclusions. +- [`RunnerFailureRule`](../subsystems/sandbox.md#wrapped-argv-and-classification-dialects) carries optional allowed exit codes, case-insensitive per-line fatal signatures, and case-insensitive exact informational-line exclusions. - [`dsh-sandbox-local`](../../packages/sandbox/sandbox-local/) maps Landlock to exit 125 plus a non-notice `landlock-run:` line while bwrap, Seatbelt, and custom runners remain signature-only. - [`dsh-bash-sandbox`](../../packages/bash/bash-sandbox/) directly spawns the provider argv, so a pre-start rejection uses the spawn-error channel instead of localized shell diagnostics. Settled foreground and background execution share one evidence-returning classifier; fatal evidence outranks denial, and foreground errors report the matched fatal line without changing captured stderr. - [`dsh-tool-fs-search`](../../packages/fs/tool-fs-search/) uses packaged ripgrep through `ctx.subprocess` and remains outside the sandboxed bash seam. diff --git a/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md b/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md index 4a31fb038c..5cf3e4422a 100644 --- a/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md +++ b/docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md @@ -40,7 +40,7 @@ stderr 仍是带内归因通道。受限子进程可以故意复现 runner 的 ## 已添加的防护措施 -- [`RunnerFailureRule`](../core-data-structures/sandbox.md#wrapped-argv-and-classification-dialects) 携带可选的允许退出码、不区分大小写的逐行致命签名,以及按不区分大小写的整行精确匹配排除的信息性行。 +- [`RunnerFailureRule`](../subsystems/sandbox.md#wrapped-argv-and-classification-dialects) 携带可选的允许退出码、不区分大小写的逐行致命签名,以及按不区分大小写的整行精确匹配排除的信息性行。 - [`dsh-sandbox-local`](../../packages/sandbox/sandbox-local/) 把 Landlock 映射为退出码 125 加一行非通知的 `landlock-run:` 诊断,而 bwrap、Seatbelt 和自定义 runner 仍仅依据签名。 - [`dsh-bash-sandbox`](../../packages/bash/bash-sandbox/) 直接 spawn 提供方 argv,因此启动前遭拒时使用 spawn 错误通道,而非本地化的 shell 诊断。已结算的前台与后台执行共用一个返回证据的分类器;致命证据优先于拒绝,前台错误会报告匹配到的致命行,同时保持捕获的 stderr 不变。 - [`dsh-tool-fs-search`](../../packages/fs/tool-fs-search/) 通过 `ctx.subprocess` 运行打包的 ripgrep,并继续位于沙箱化 bash seam 之外。 diff --git a/packages/cordis/README.i18n.yaml b/docs/subsystems/README.i18n.yaml similarity index 59% rename from packages/cordis/README.i18n.yaml rename to docs/subsystems/README.i18n.yaml index 2eacddb52c..f1636e0191 100644 --- a/packages/cordis/README.i18n.yaml +++ b/docs/subsystems/README.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 packages/cordis/README.md -README.md: e8b0790f911d1acc1216d412191ca967e0e1ac27 -README.zh.md: c452f9a7ef69e7f5af8900153271d500f19f9e47 +# pnpm run verify-translation-pairing --write docs/subsystems/README.md +README.md: 1c3d14aeb4fd11cbc45eaadd83f0b84de0dfda52 +README.zh.md: dda61d86c4d00117af851b4a55d16b97c6bc2f10 diff --git a/docs/subsystems/README.md b/docs/subsystems/README.md new file mode 100644 index 0000000000..1c3d14aeb4 --- /dev/null +++ b/docs/subsystems/README.md @@ -0,0 +1,51 @@ +# Subsystems + +English | [中文](README.zh.md) + +One page per subsystem of the DeepSeek Harness: what it is, the data structures it moves, and — where a `ctx` service or event scope backs it — a generated **Cordis surface** section carrying its service and event reference. The folder complements [architecture.md](../architecture.md), which describes *behavior* across subsystems (the service map, the session/turn/step lifecycle, the event taxonomy); each page here is the reference for one subsystem's vocabulary and wiring. + +| Page | Owns | +|---|---| +| [core.md](core.md) | the `packages/core` control spine: the package-by-package loop map, agent creation and ownership (`AgentHandle`), the `Agent` handle with its delivery/cancellation/interception contracts, and the repo-wide type patterns (`…Map → derived-union`, branded ids) | +| [llm-streaming.md](llm-streaming.md) | the `packages/llm` conversation vocabulary — `Message`/`ContentBlock`, the assembled model request, the `StreamChunk` wire protocol + adapter contract, `BlockAssembler`, the `LlmAdapter` seam | +| [token-meter.md](token-meter.md) | immutable scalar and positional replay measurements with consumed-log revisions | +| [scope.md](scope.md) | scoped registration identity, dispatch carriers, and the owned `Scope` context | +| [typert.md](typert.md) | Remote invocation descriptors, lookup/Context declarations, TypeRT registries, and the Host Gateway/Client API seams | +| [goal.md](goal.md) | persisted goal identity, lifecycle snapshots, activation, change records, and round attribution | +| [commands.md](commands.md) | the human-command seam: definitions, adapter discovery, direct invocation, results, and parsing views | +| [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnTrigger`/`TurnEndReason`, `deriveMessages()`, execution enclosure, and standalone events | +| [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, JSONL + SQLite backends, `session/flush`, crash recovery, `SessionHeader` | +| [settings.md](settings.md) | the user-settings seam: `SettingsNamespace` registration, layered resolution (defaults → composition `base` → user document), owner scopes, hot commits | +| [credentials.md](credentials.md) | the credential seam: `CredentialRef` references (never values) in configuration, per-operation resolution, UI-safe `CredentialInfo`, provider source layers | +| [session-query.md](session-query.md) | logical records, bounded exact-event reads, relationship traces, semantic filters/documents, and full-text result pages | +| [session-title.md](session-title.md) | durable title snapshots, source provenance, and the asynchronous provider contract | +| [session-reference.md](session-reference.md) | structured cross-session references: `SessionReferenceInput`/`Candidate`, prepared message contexts, the stable error taxonomy | +| [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, prompt sections, and cooperative assembly | +| [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, and the guarded execution pipeline | +| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy | +| [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit and answerer contracts | +| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashProcess` handles | +| [subprocess.md](subprocess.md) | the subprocess seam: fully-explicit `SubprocessSpawnSpec`, offset-based output readers, unclassified `SubprocessOutcome`, and the managed `DSH_*` environment vocabulary | +| [pty.md](pty.md) | persistent terminal ids, backend/session contracts, send readiness, bounded reads, and owner-visible snapshots | +| [sandbox.md](sandbox.md) | per-session policy resolution and the process-confinement seam: file-effect modes, execution/provider policies, `ConfinedArgv`, enforcement and fail-closed errors | +| [code-runtime.md](code-runtime.md) | the code-execution seam: `CodeRunRequest`/`Result`, binding namespaces, captured logs, the `CodeRunFailure` taxonomy | +| [filesystem.md](filesystem.md) | the filesystem seam: `FsTarget`, read/write/edit outcomes, observed-file state, `FsErrorCode` | +| [lsp.md](lsp.md) | the LSP navigation seam: `LspQueryRequest`/`Result`, `LspProvider`/`Service`, four operations, `LspError` | +| [skills.md](skills.md) | the skill service: discovery priority, `SkillSummary`/`SkillDefinition`, session-prefix catalog, model-facing `skill` loading | +| [compaction.md](compaction.md) | the compaction seam: the `compact/*` session events, `CompactionResult`, the `CompactService` interface | +| [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split | +| [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` | +| [spill.md](spill.md) | the spill storage seam: `SaveTextSpill`, `SpillOwner`/`SpillSource`, `SpillRef`, the branded `SpillLocator` | +| [workflow.md](workflow.md) | the workflow seam: `WorkflowStartRequest`, `WorkflowMeta`, `WorkflowRun`/`Result`, the `workflow/*` event payloads, `WorkflowError` fatality | +| [tasks.md](tasks.md) | the background-task runtime: branded `TaskId`s, the producer contract, consumer views, `ctx.tasks` service behavior | +| [permission.md](permission.md) | the permission-preset layer: `PresetSpec`/`PresetOption`, the derived `custom` state, the log-only `permission/preset` event | +| [plan.md](plan.md) | plan mode: the log-only `plan/mode` state, pending-selection flush, `PlanModeConfig`, the `exit_plan_mode` review arc | +| [invariants.md](invariants.md) | the runtime-invariant registry: selection `Config`, `InvariantInstaller`/`InvariantFailure`, the empty-companion contract | +| [http-server.md](http-server.md) | the HTTP carrier: `WebRouteKind`/`WebRoute`, match order, the claimable fallback seat, index taps | +| [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 and events use each page's generated **Cordis surface** section. diff --git a/docs/subsystems/README.zh.md b/docs/subsystems/README.zh.md new file mode 100644 index 0000000000..dda61d86c4 --- /dev/null +++ b/docs/subsystems/README.zh.md @@ -0,0 +1,51 @@ +# 子系统 + +[English](README.md) | 中文 + +每个子系统一页,覆盖 DeepSeek Harness 的全部子系统:它是什么、它操作哪些数据结构,以及——当它由某个 `ctx` 服务或事件作用域支撑时——一段生成的 **Cordis surface** 小节,承载其服务与事件参考。本目录与 [architecture.md](../architecture.md) 互补:后者描述跨子系统的*行为*(服务映射、会话/轮次/步骤生命周期、事件分类体系);这里的每一页是单个子系统词汇与接线的参考。 + +| 页面 | 负责内容 | +|---|---| +| [core.md](core.md) | `packages/core` 控制主干:逐包循环地图、agent 创建与所有权(`AgentHandle`)、`Agent` 句柄及其投递/取消/拦截契约,以及全仓通用类型模式(`…Map → 派生联合`、品牌化 id) | +| [llm-streaming.md](llm-streaming.md) | `packages/llm` 的对话词汇——`Message`/`ContentBlock`、组装完成的模型请求、`StreamChunk` 协议格式(wire format)+ 适配器契约(adapter contract)、`BlockAssembler`、`LlmAdapter` seam | +| [token-meter.md](token-meter.md) | 不可变的标量与位置回放度量,附带已消费日志修订号 | +| [scope.md](scope.md) | 作用域注册标识、dispatch 载体,以及拥有的 `Scope` 上下文 | +| [typert.md](typert.md) | 远程调用描述符、lookup/Context 声明、TypeRT 注册表,以及 Host Gateway/Client API seam | +| [goal.md](goal.md) | 持久 goal 标识、生命周期快照、激活、变更记录与 Round 归属 | +| [commands.md](commands.md) | 人类命令 seam:定义、适配器发现、直接调用、结果与解析视图 | +| [session.md](session.md) | 完整的 `SessionEventMap` 变体目录、`TurnTrigger`/`TurnEndReason`、`deriveMessages()`、执行封闭与独立事件 | +| [persistence.md](persistence.md) | 持久性 seam:`SessionPersistence`、JSONL + SQLite 后端、`session/flush`、崩溃恢复、`SessionHeader` | +| [settings.md](settings.md) | 用户设置 seam:`SettingsNamespace` 注册、分层解析(默认值 → 组合 `base` → 用户文档)、owner scope、热提交 | +| [credentials.md](credentials.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、provider 来源层 | +| [session-query.md](session-query.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 | +| [session-title.md](session-title.md) | 持久标题快照、来源 provenance 与异步提供方契约 | +| [session-reference.md](session-reference.md) | 结构化跨会话引用:`SessionReferenceInput`/`Candidate`、prepared 消息上下文、稳定错误分类 | +| [system-prompt.md](system-prompt.md) | 逐次组装的上下文、工具提供方结果、提示词段落与协作式组装 | +| [tools.md](tools.md) | `ToolDefinition` 完整字段、schema DSL、`ToolExecution`/`ToolResult`、工具展示 UI 类型,以及受保护的执行流水线 | +| [user-interaction.md](user-interaction.md) | UI 支持的人工问答 seam:`AskUserQuestionRequest`、answer/options 词汇、提供方 API、错误分类体系 | +| [approval.md](approval.md) | 一次性用户审批 seam:`ApprovalRequest`、`ApprovalOutcome`、逐会话策略、审计与 answerer 契约 | +| [bash.md](bash.md) | bash 执行器 seam:`BashExecRequest`/`Spec`、`BashRunResult`、后台 `BashProcess` 句柄 | +| [subprocess.md](subprocess.md) | 子进程 seam:完全显式的 `SubprocessSpawnSpec`、基于偏移的输出读取器、不含分类的 `SubprocessOutcome`,以及受管 `DSH_*` 环境词汇 | +| [pty.md](pty.md) | 持久化终端 ID、后端/会话契约、发送就绪状态、有界读取与 owner 可见快照 | +| [sandbox.md](sandbox.md) | 每会话策略解析与进程约束 seam:文件效果模式、执行/提供方策略、`ConfinedArgv`、强制执行与故障关闭错误 | +| [code-runtime.md](code-runtime.md) | 代码执行 seam:`CodeRunRequest`/`Result`、绑定命名空间、捕获日志、`CodeRunFailure` 分类体系 | +| [filesystem.md](filesystem.md) | 文件系统 seam:`FsTarget`、读/写/编辑结果、观测到的文件状态、`FsErrorCode` | +| [lsp.md](lsp.md) | LSP 导航 seam:`LspQueryRequest`/`Result`、`LspProvider`/`Service`、四种操作、`LspError` | +| [skills.md](skills.md) | skill(技能)服务:发现优先级、`SkillSummary`/`SkillDefinition`、会话前缀目录、面向模型的 `skill` 加载 | +| [compaction.md](compaction.md) | 压缩(compaction)seam:`compact/*` 会话事件、`CompactionResult`、`CompactService` 接口 | +| [subagent.md](subagent.md) | subagent seam:命名提供方注册表、`SubagentStartRequest`/`Result`/`Run`、启动时与运行时能力拆分 | +| [web.md](web.md) | Web 访问 seam:`WebSearchRequest`/`Result`、`WebFetchRequest`/`Result`、`WebFetchBody`、提供方可用性、`WebError` | +| [spill.md](spill.md) | spill 存储 seam:`SaveTextSpill`、`SpillOwner`/`SpillSource`、`SpillRef`、品牌类型 `SpillLocator` | +| [workflow.md](workflow.md) | 工作流 seam:`WorkflowStartRequest`、`WorkflowMeta`、`WorkflowRun`/`Result`、`workflow/*` 事件载荷、`WorkflowError` 致命性 | +| [tasks.md](tasks.md) | 后台任务运行时:品牌化 `TaskId`、producer 契约、consumer 视图、`ctx.tasks` 服务行为 | +| [permission.md](permission.md) | 权限预设层:`PresetSpec`/`PresetOption`、派生的 `custom` 状态、仅记日志的 `permission/preset` 事件 | +| [plan.md](plan.md) | 计划模式:仅记日志的 `plan/mode` 状态、待定选择的冲刷、`PlanModeConfig`、`exit_plan_mode` 审阅流程 | +| [invariants.md](invariants.md) | 运行时不变式注册表:选择配置 `Config`、`InvariantInstaller`/`InvariantFailure`、空配套插件契约 | +| [http-server.md](http-server.md) | HTTP 载体:`WebRouteKind`/`WebRoute`、匹配顺序、可认领的回退席位、index 转换 | +| [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 surface** 小节。 diff --git a/docs/core-data-structures/bash.i18n.yaml b/docs/subsystems/approval.i18n.yaml similarity index 56% rename from docs/core-data-structures/bash.i18n.yaml rename to docs/subsystems/approval.i18n.yaml index 30e15dbacc..93c56707bf 100644 --- a/docs/core-data-structures/bash.i18n.yaml +++ b/docs/subsystems/approval.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/approval.md +approval.md: 2045f2073b72fa265728cbf77b673053a2eaf371 +approval.zh.md: c021db14c4925acaeef5d984d9dfa88ad55bd8dd diff --git a/docs/subsystems/approval.md b/docs/subsystems/approval.md new file mode 100644 index 0000000000..2045f2073b --- /dev/null +++ b/docs/subsystems/approval.md @@ -0,0 +1,171 @@ +# User Approval + +English | [中文](approval.zh.md) + +The user-approval seam of [dsh-user-approval](../../packages/interaction/user-approval) answers one question: may this specific action proceed? It owns the shared request/outcome vocabulary, the `ctx.approval` dispatch service, the `approval/request` answerer waterfall, the log-only audit pair, and the per-session `ask`/`never` policy. UI channels may provide human answerers; the [ACP automation bridge](../../packages/acp/acp) provides one-shot machine decisions for its own agents. Callers such as [dsh-tools](../../packages/core/tools) and [dsh-tool-bash](../../packages/bash/tool-bash) consume the closed outcome and fail closed unless it is `allowed-once`. + +Source: [`packages/interaction/user-approval/src/index.ts`](../../packages/interaction/user-approval/src/index.ts) + +## Identity and outcome + +Every request receives a fresh `ApprovalRequestId`. The brand pairs the `approval/asked` and `approval/decided` audit events without making approval ids interchangeable with tool-call or agent/session ids. + +```ts type-equiv +/** + * Pairs one `approval/asked` audit event with its `approval/decided`. + * Service-issued (one fresh id per {@link ApprovalService.request} call). + */ +type ApprovalRequestId = Branded<'ApprovalRequestId'> +``` + +`ApprovalOutcome` is closed and fail-closed. `allowed-once` grants only the asked-about action; callers deny on `rejected`, `cancelled`, and `unavailable`. A missing, non-owning, throwing, or non-conforming answerer becomes `unavailable` rather than opening the gate. + +```ts type-equiv +/** + * Closed approval outcomes: a one-shot grant, explicit rejection, withdrawn + * request, or unavailable answerer. Callers fail closed on `unavailable`. + */ +type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable' +``` + +## Per-session policy + +`ApprovalPolicy` determines what happens before interactive answerers run. `ask` delegates to the composed answerer chain, whose no-answer default is `unavailable`; `never` deterministically returns `rejected` without dispatching any answerer. The effective value is the last `approval/policy` event in the session log, falling back to the service config. `setApprovalPolicy(session, policy)` is the single write path, so replay reconstructs the override. + +```ts type-equiv +/** + * A session's approval policy — what happens to an {@link ApprovalService} + * ask BEFORE any interactive answerer sees it: + * + * - `'ask'` (the default) — delegate to the composed answerers; with none + * composed the chain falls through to the fail-closed `'unavailable'` + * (exactly today's behavior). + * - `'never'` — never prompt anyone: every ask resolves `'rejected'` + * deterministically. The strict headless stance (CI, unattended runs) and + * the policy whose outcome is knowable without asking. + */ +type ApprovalPolicy = 'ask' | 'never' +``` + +Both policies contribute their complete current meaning to the cache-safe runtime-context snapshot. The sourced `user/message` is the durable model-visible input; changing approval state appends a new full snapshot after retained history without rewriting the request header's system prompt. + +## Approval request + +`ApprovalRequest` identifies the agent and tool action closely enough to route and audit the question. It deliberately omits tool arguments: an answerer attaches the prompt to the already-streamed tool call through `callId` instead of rendering a second copy that could drift. + +```ts type-equiv +/** + * Readonly same-process permission question. `callId` links to an already + * presented tool call, so arguments are not duplicated here. + */ +interface ApprovalRequest { + /** + * The agent on whose behalf the question is asked. Routes the question (a + * UI answerer only answers for agents it owns) and receives the audit + * events on its session log. + */ + readonly agent: Agent + /** The tool the question is about (presentation and audit). */ + readonly toolName: string + /** + * The exact tool call being decided, when the asker has one — lets a UI + * attach the prompt to the tool call it already streamed. + */ + readonly callId?: CallId + /** The asker's human-readable explanation of WHY it is asking. */ + readonly reason?: string + /** + * Aborting withdraws the question: the request settles `'cancelled'` + * immediately and a late answer from a still-pending answerer is discarded. + */ + readonly signal?: AbortSignal +} +``` + +## Dispatch and audit + +`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 new file mode 100644 index 0000000000..c021db14c4 --- /dev/null +++ b/docs/subsystems/approval.zh.md @@ -0,0 +1,171 @@ +# 用户审批 + +[English](approval.md) | 中文 + +[dsh-user-approval](../../packages/interaction/user-approval) 的用户审批 seam 回答一个问题:这个具体操作是否可以继续?它拥有共享的请求/结果词汇、`ctx.approval` 分发服务、`approval/request` 应答者 waterfall(瀑布式事件)、仅记录日志的审计事件对,以及按会话的 `ask`/`never` 策略。UI 通道可以提供人类应答者;[ACP(Agent Client Protocol)自动化桥接层](../../packages/acp/acp)为其拥有的 agent(智能体)提供一次性机器决策。调用方如 [dsh-tools](../../packages/core/tools) 和 [dsh-tool-bash](../../packages/bash/tool-bash) 消费闭合的结果,除非结果为 `allowed-once`,否则一律拒绝。 + +源码:[`packages/interaction/user-approval/src/index.ts`](../../packages/interaction/user-approval/src/index.ts) + +## 标识与结果 + +每个请求都会获得一个全新的 `ApprovalRequestId`。该品牌类型将 `approval/asked` 与 `approval/decided` 审计事件配对,同时不会让审批 id 与工具调用 id 或 agent/会话 id 互换。 + +```ts type-equiv +/** + * Pairs one `approval/asked` audit event with its `approval/decided`. + * Service-issued (one fresh id per {@link ApprovalService.request} call). + */ +type ApprovalRequestId = Branded<'ApprovalRequestId'> +``` + +`ApprovalOutcome` 是闭合的,且失败时拒绝。`allowed-once` 仅授权所询问的那一个操作;调用方对 `rejected`、`cancelled` 和 `unavailable` 均执行拒绝。缺失、不负责该请求、抛异常或不合规的应答者会产生 `unavailable`,而非放行。 + +```ts type-equiv +/** + * Closed approval outcomes: a one-shot grant, explicit rejection, withdrawn + * request, or unavailable answerer. Callers fail closed on `unavailable`. + */ +type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable' +``` + +## 按会话策略 + +`ApprovalPolicy` 决定在交互式应答者运行之前发生什么。`ask` 委托给组合的应答者链,链的无应答默认值为 `unavailable`;`never` 确定性地返回 `rejected`,不分发任何应答者。生效值为会话日志中最后一条 `approval/policy` 事件,回退到服务配置。`setApprovalPolicy(session, policy)` 是唯一的写入路径,因此回放能重建覆盖值。 + +```ts type-equiv +/** + * A session's approval policy — what happens to an {@link ApprovalService} + * ask BEFORE any interactive answerer sees it: + * + * - `'ask'` (the default) — delegate to the composed answerers; with none + * composed the chain falls through to the fail-closed `'unavailable'` + * (exactly today's behavior). + * - `'never'` — never prompt anyone: every ask resolves `'rejected'` + * deterministically. The strict headless stance (CI, unattended runs) and + * the policy whose outcome is knowable without asking. + */ +type ApprovalPolicy = 'ask' | 'never' +``` + +两种策略都会将各自完整的当前含义贡献给缓存安全的运行时上下文快照。带来源的 `user/message` 是持久化且模型可见的输入;批准状态变化时,会在保留的历史后追加一份新的完整快照,而不改写请求头中的系统提示词。 + +## 审批请求 + +`ApprovalRequest` 以足够精确的方式标识 agent 和工具操作,以便路由和审计该问题。它有意省略工具参数:应答者通过 `callId` 将提示附加到已流式输出的工具调用上,而非渲染一份可能漂移的副本。 + +```ts type-equiv +/** + * Readonly same-process permission question. `callId` links to an already + * presented tool call, so arguments are not duplicated here. + */ +interface ApprovalRequest { + /** + * The agent on whose behalf the question is asked. Routes the question (a + * UI answerer only answers for agents it owns) and receives the audit + * events on its session log. + */ + readonly agent: Agent + /** The tool the question is about (presentation and audit). */ + readonly toolName: string + /** + * The exact tool call being decided, when the asker has one — lets a UI + * attach the prompt to the tool call it already streamed. + */ + readonly callId?: CallId + /** The asker's human-readable explanation of WHY it is asking. */ + readonly reason?: string + /** + * Aborting withdraws the question: the request settles `'cancelled'` + * immediately and a late answer from a still-pending answerer is discarded. + */ + readonly signal?: AbortSignal +} +``` + +## 分发与审计 + +`ctx.approval.request(req)` 要求发起请求的会话处于一个尚未结束的轮次内。它追加 `approval/asked`,获取一个结果,追加对应的 `approval/decided`,然后以该结果完成。`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/packages/ui/README.i18n.yaml b/docs/subsystems/bash.i18n.yaml similarity index 57% rename from packages/ui/README.i18n.yaml rename to docs/subsystems/bash.i18n.yaml index fba1f95c52..f6ed20cace 100644 --- a/packages/ui/README.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 packages/ui/README.md -README.md: 15754410a4a81eb3fc898dd55269ddd1637e1dab -README.zh.md: 4023b80085998f57ed321bfda3a0abdd08b70a28 +# pnpm run verify-translation-pairing --write docs/subsystems/bash.md +bash.md: dd7649ce46bea5642270fa553eb3d82f2dc33e9b +bash.zh.md: bba9ece2955cee66e941aa878299cff6c06a97a0 diff --git a/docs/core-data-structures/bash.md b/docs/subsystems/bash.md similarity index 75% rename from docs/core-data-structures/bash.md rename to docs/subsystems/bash.md index f83a133c8e..dd7649ce46 100644 --- a/docs/core-data-structures/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/core-data-structures/bash.zh.md b/docs/subsystems/bash.zh.md similarity index 75% rename from docs/core-data-structures/bash.zh.md rename to docs/subsystems/bash.zh.md index 27ca9f6e5a..bba9ece295 100644 --- a/docs/core-data-structures/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/core-data-structures/compaction.i18n.yaml b/docs/subsystems/client-modules.i18n.yaml similarity index 54% rename from docs/core-data-structures/compaction.i18n.yaml rename to docs/subsystems/client-modules.i18n.yaml index 2ad9ea8e8a..ae61603169 100644 --- a/docs/core-data-structures/compaction.i18n.yaml +++ b/docs/subsystems/client-modules.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/compaction.md -compaction.md: f1df5b83bd43136af60988dabd9dc68fe32467a2 -compaction.zh.md: 52540250d466f81f51cf7c681bb6f1436e29a12d +# pnpm run verify-translation-pairing --write docs/subsystems/client-modules.md +client-modules.md: 19bcc6304e9d917c3950fd9857e7b2c2b3c16e73 +client-modules.zh.md: b0ad293cad2f67de1982ef271308b2a7f06ebaf4 diff --git a/docs/subsystems/client-modules.md b/docs/subsystems/client-modules.md new file mode 100644 index 0000000000..19bcc6304e --- /dev/null +++ b/docs/subsystems/client-modules.md @@ -0,0 +1,118 @@ +# Client Modules + +English | [中文](client-modules.zh.md) + +The web plugin table: the Node half of the client module system in [dsh-client-modules](../../packages/client/modules), provided as `ctx.clientModuleHost` (`ClientModuleHostService`). It scans the host Loader's entries for `dshClient` packages, composes the `window.__DSH_BOOT__` entry graph, serves each bundle at `/plugins//client.js`, and taps the index render to inject the boot manifest — the four faces of one service. It is an optional capability of the web GUI stack, not part of the agent-loop spine, and it is a consumer of [dsh-host-webserver](../../packages/host/webserver): the carrier described in [http-server.md](http-server.md) supplies the prefix route and index tap this service registers. The same package's browser half (`ctx.modules`, the lazy-CJS module table that fetches and materializes these bundles) is kernel machinery documented in the [package README](../../packages/client/modules/README.md), not here. + +Source: [`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts) + +## The wire + +The graph is the wire single source between the Node and browser halves: the host composes `WebBootEntry` rows from scanned packages, injects the graph as the first script in `` (`window.__DSH_BOOT__`, with `<` escaped so plugin-controlled strings cannot break out of the script element), and the shell parses it before booting anything. A page without a valid manifest cannot boot — the browser-side parser throws loud on a missing or malformed graph. + +```ts type-equiv +/** + * One composed client entry pushed by the host (web2 §0 graph row). Wire + * single source: the host node half (package root) produces this same shape. + * `immediately` marks stage-one prefetch; `inject` is informational graph + * metadata (the authoritative edges live in each package's dshClient + * declaration and reach fibers through entry creation). + */ +interface WebBootEntry { + /** Entry name == package name. */ + id: string + /** Bundle endpoint, '/plugins//client.js?rev='. */ + url: string + /** Bundle content hash (cache-busting consistency anchor). */ + rev: string + /** Package-name dependency edges, informational (preflight display / HMR diffing). */ + inject?: string[] + /** Stage-one prefetch mark: load the script for factory registration during module-face boot. */ + immediately?: boolean +} +``` + +```ts type-equiv +/** The composed client entry graph the host injects as `window.__DSH_BOOT__`. */ +interface WebBootGraph { + /** Consistency anchor over the whole graph (content + bundle hashes). */ + rev: string + /** Composed entries; order carries no semantics (activation order is fiber inject waiting). */ + entries: WebBootEntry[] +} +``` + +Each row's `rev` is the bundle's content hash and rides the URL as a cache-busting query; the graph `rev` hashes the composed rows, so any row change changes it. `immediately` marks the stage-one prefetch tier (fetch and execute during module-face boot, registration only); a lazy row is fetched on first import. + +## The scan + +A package joins the table by declaring `dshClient` (`platform: 'web'`, optional `inject` edges, optional `immediately`) in its package.json and exporting its built bundle at `exports["./client"]`. Package resolution anchors at the config tree's `ctx.baseUrl` — the cordis.yml directory, whose package declares every composed plugin as a dependency — and construction throws when that anchor is unset. + +Scanning is incremental per package; there is no full-rescan code path. Every cordis `internal/plugin` emission (fiber construction or disposal) marks the fiber's entry name dirty, and a microtask flush reconciles each dirty name against the live loader entries. The activation pass seeds the same dirty set with all current entries and flushes synchronously, so first scan and steady state share one implementation — with opposite failure postures. At activation, a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud `AggregateError` listing every broken package: the fiber FAILS and the boot's fail-loud sweep reports it. In steady state, a broken package logs a warning and must not poison the others. + +Package metadata — including the negative "not a client package" verdict — is cached per name and never expires: plugin-set changes take effect on restart. A fiber restart reuses its row and rev untouched; bundle content changes reach the graph only through `rebuilt()`. + +## The bundle route and index tap + +`GET`/`HEAD /plugins//client.js` serves the registered bundle from disk with `no-cache` (the rev query, not HTTP caching, anchors consistency); other methods are 405. An unknown id — or a registered row whose bundle is unreadable because it has not been built yet — answers a loud 404 rather than letting the carrier's SPA fallback ship HTML as JavaScript. The index tap injects the current graph on every index render, so a reload always boots against the live composition. + +## 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](#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 new file mode 100644 index 0000000000..b0ad293cad --- /dev/null +++ b/docs/subsystems/client-modules.zh.md @@ -0,0 +1,118 @@ +# Client 模块 + +[English](client-modules.md) | 中文 + +Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client 模块系统的 Node 半,以 `ctx.clientModuleHost`(`ClientModuleHostService`)形式提供。它扫描宿主 Loader 的 entry 找出 `dshClient` 包(package),组合出 `window.__DSH_BOOT__` entry 图,在 `/plugins//client.js` 提供各个 bundle,并经 index 转换(index tap)注入启动 manifest(元数据清单)——这是同一个服务的四个面。它是 Web GUI 栈的一项可选能力,不属于 agent loop(智能体循环)主干,并且是 [dsh-host-webserver](../../packages/host/webserver) 的消费方:[http-server.md](http-server.md) 所述的载体提供本服务注册的前缀路由与 index 转换。同一个包的浏览器半(`ctx.modules`,即拉取并物化这些 bundle 的 lazy CJS 模块表)属于内核机件,记录在[包 README](../../packages/client/modules/README.md)中,不在本页。 + +源码:[`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts) + +## wire + +图是 Node 半与浏览器半之间协议层的唯一事实来源:宿主从扫描到的包组合出 `WebBootEntry` 行,把图作为 `` 中的第一个脚本注入(`window.__DSH_BOOT__`,其中 `<` 已转义,插件可控的字符串因此无法逃出 script 元素),壳则在启动任何东西之前先解析它。没有有效 manifest 的页面无法启动——浏览器侧的解析器在图缺失或畸形时大声抛错。 + +```ts type-equiv +/** + * One composed client entry pushed by the host (web2 §0 graph row). Wire + * single source: the host node half (package root) produces this same shape. + * `immediately` marks stage-one prefetch; `inject` is informational graph + * metadata (the authoritative edges live in each package's dshClient + * declaration and reach fibers through entry creation). + */ +interface WebBootEntry { + /** Entry name == package name. */ + id: string + /** Bundle endpoint, '/plugins//client.js?rev='. */ + url: string + /** Bundle content hash (cache-busting consistency anchor). */ + rev: string + /** Package-name dependency edges, informational (preflight display / HMR diffing). */ + inject?: string[] + /** Stage-one prefetch mark: load the script for factory registration during module-face boot. */ + immediately?: boolean +} +``` + +```ts type-equiv +/** The composed client entry graph the host injects as `window.__DSH_BOOT__`. */ +interface WebBootGraph { + /** Consistency anchor over the whole graph (content + bundle hashes). */ + rev: string + /** Composed entries; order carries no semantics (activation order is fiber inject waiting). */ + entries: WebBootEntry[] +} +``` + +每一行的 `rev` 是该 bundle 的内容哈希,并作为使缓存失效的查询参数附在 URL 上;图的 `rev` 对组合后的各行做哈希,因此任何一行的变化都会改变它。`immediately` 标记第一阶段预取档位(在模块面启动期间 fetch 并执行,只做登记);惰性行在首次 import 时才拉取。 + +## 扫描 + +包加入这张表的方式,是在自己的 package.json 中声明 `dshClient`(`platform: 'web'`、可选的 `inject` 边、可选的 `immediately`),并在 `exports["./client"]` 导出构建好的 bundle。包解析锚定在配置树的 `ctx.baseUrl`——即 cordis.yml 所在目录,该目录的包把每个被组合的插件声明为依赖——这一锚点未设置时,构造即抛错。 + +扫描是单包增量的;不存在全量重扫代码路径。fiber 构造或 dispose(资源释放)时的每次 cordis `internal/plugin` 发射都把该 fiber 的 entry 名标脏,一次微任务 flush 把每个脏名与实时 loader entry 对账。激活趟以全部当前 entry 灌入同一个脏集合并同步 flush,因此初扫与稳态共享一条实现——但失败姿态相反。激活时,已加载 entry 中的畸形声明或缺失 bundle 会聚合为一个大声的 `AggregateError`,列出每个损坏的包:该 fiber 进入 FAILED,由启动的大声失败 sweep 上报。稳态下,损坏的包只记录一条警告,且不得殃及其他包。 + +包元数据——包括「非 client 包」这一否定结论——按名缓存且永不过期:插件集合的变更在重启后生效。fiber 重启原样复用其行与 rev;bundle 内容变更只经 `rebuilt()` 到达图。 + +## bundle 路由与 index 转换 + +`GET`/`HEAD /plugins//client.js` 以 `no-cache` 从磁盘提供已注册的 bundle(锚定一致性的是 rev 查询参数,而非 HTTP 缓存);其他方法返回 405。未知 id——或已注册、但 bundle 因尚未构建而不可读的行——回应一个大声的 404,而不是让载体的 SPA 回退把 HTML 当作 JavaScript 发出。index 转换在每次 index 渲染时注入当前图,因此刷新页面总是针对实时组合启动。 + +## 服务 + +`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/core-data-structures/commands.i18n.yaml b/docs/subsystems/code-runtime.i18n.yaml similarity index 55% rename from docs/core-data-structures/commands.i18n.yaml rename to docs/subsystems/code-runtime.i18n.yaml index 532151ef70..05e7e67ed1 100644 --- a/docs/core-data-structures/commands.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/commands.md -commands.md: 1390f736ff582a997ee710cfd27fce4e7db3461f -commands.zh.md: 53aec9bd138773df7fceb45e5d6c30b7205dd0d9 +# pnpm run verify-translation-pairing --write docs/subsystems/code-runtime.md +code-runtime.md: 12779f1aafc8930410ae0d695e3f617caafcf607 +code-runtime.zh.md: de563dd3430348f5cf3675d4bf124adeba170559 diff --git a/docs/core-data-structures/code-runtime.md b/docs/subsystems/code-runtime.md similarity index 83% rename from docs/core-data-structures/code-runtime.md rename to docs/subsystems/code-runtime.md index fbce7d812b..12779f1aaf 100644 --- a/docs/core-data-structures/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/core-data-structures/code-runtime.zh.md b/docs/subsystems/code-runtime.zh.md similarity index 83% rename from docs/core-data-structures/code-runtime.zh.md rename to docs/subsystems/code-runtime.zh.md index 700146cfaa..de563dd343 100644 --- a/docs/core-data-structures/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/core-data-structures/core.i18n.yaml b/docs/subsystems/commands.i18n.yaml similarity index 56% rename from docs/core-data-structures/core.i18n.yaml rename to docs/subsystems/commands.i18n.yaml index ee870f9334..3bd5420573 100644 --- a/docs/core-data-structures/core.i18n.yaml +++ b/docs/subsystems/commands.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/core.md -core.md: 8f413a7a064ad6f63e0caec31354869e51139020 -core.zh.md: d0f02f0cfc2cd30fc67aacf5b17d1daf324295c5 +# pnpm run verify-translation-pairing --write docs/subsystems/commands.md +commands.md: c1ab6d9782f515321104d0cf4d9e037c6a68a18d +commands.zh.md: 3b3d8def042b107c5098e76c511424f023975e77 diff --git a/docs/subsystems/commands.md b/docs/subsystems/commands.md new file mode 100644 index 0000000000..c1ab6d9782 --- /dev/null +++ b/docs/subsystems/commands.md @@ -0,0 +1,185 @@ +# Human Commands + +English | [中文](commands.zh.md) + +The human-command seam of [`dsh-commands`](../../packages/interaction/commands). Interactive adapters use it to discover and directly execute plugin-owned commands for an exact agent without creating a model message. The [command Agent Note](../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md) owns dispatch and lifecycle rationale; the [package README](../../packages/interaction/commands/README.md) owns composition and limitations. + +Source: [`packages/interaction/commands/src/index.ts`](../../packages/interaction/commands/src/index.ts) + +## Input metadata + +The seam exposes one optional unstructured-input hint. Command availability follows plugin composition: every adapter consuming the registry sees every effective definition. + +```ts type-equiv +/** Immutable metadata for a command's optional unstructured input. */ +interface CommandInputDescriptor { + /** Placeholder shown before the user supplies free-form input. */ + readonly hint: string +} +``` + +## Definition + +`CommandDefinition` is the plugin-authored registration. The registry validates and freezes a detached effective definition. + +```ts type-equiv +/** Plugin-owned command registration. */ +interface CommandDefinition { + /** Lowercase command name without the leading slash. */ + readonly name: string + /** Human-readable summary used in discovery UI. */ + readonly description: string + /** Optional free-form input hint advertised to capable clients. */ + readonly input?: CommandInputDescriptor + /** + * Whether `command/run` records `rawInput`. Defaults to true. A command + * whose domain event owns the payload sets this false to avoid duplicating + * that payload in the session log. + */ + readonly recordInput?: boolean + /** Execute against the receiving agent without sending the command to the model. */ + readonly handler: (invocation: CommandInvocation) => CommandResult | Promise +} +``` + +## Invocation and result + +The adapter owns cancellation and passes the exact target agent. `rawInput` begins immediately after the parsed name and retains the adapter-delivered separator and suffix. Results are direct UI outcomes, not tool results or session events. + +```ts type-equiv +/** Invocation passed to one registered command handler. */ +interface CommandInvocation { + /** Exact agent whose human-facing surface received the command. */ + readonly agent: Agent + /** Exact text following the registered command name, including separator whitespace. */ + readonly rawInput: string + /** Cancellation signal owned by the dispatching UI request. */ + readonly signal: AbortSignal +} +``` + +```ts type-equiv +/** Expected command outcome rendered directly by the dispatching UI. */ +type CommandResult = + | { + readonly kind: 'success' + readonly text?: string + /** Earlier authoritative domain event that owns a richer presentation. */ + readonly sourceEventSeq?: number + } + | { readonly kind: 'error'; readonly text: string } +``` + +`sourceEventSeq` is optional and success-only. When present, it names an earlier non-command event in the receiving session log; `command/done` persists the same reference so a client can combine the command lifecycle with that domain projection without parsing `text` or relying on adjacent rows. + +## Discovery and parsing views + +Adapters receive handler-free immutable descriptors after scope resolution. `parseCommand()` returns `ParsedCommand` before registry resolution; syntax-valid input can still name an unavailable command. + +```ts type-equiv +/** Handler-free immutable command view returned to UI adapters. */ +interface CommandDescriptor { + /** Lowercase command name without the leading slash. */ + readonly name: string + /** Human-readable summary used in discovery UI. */ + readonly description: string + /** Optional free-form input hint advertised to capable clients. */ + readonly input?: CommandInputDescriptor +} +``` + +```ts type-equiv +/** Syntactically valid slash command before registry resolution. */ +interface ParsedCommand { + /** Lowercase command name without the leading slash. */ + readonly name: string + /** Exact text following the command name. */ + 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 new file mode 100644 index 0000000000..3b3d8def04 --- /dev/null +++ b/docs/subsystems/commands.zh.md @@ -0,0 +1,185 @@ +# 用户命令 + +[English](commands.md) | 中文 + +[`dsh-commands`](../../packages/interaction/commands) 的用户命令 seam。交互式适配器用它发现插件拥有的命令,并针对确切的 agent(智能体)直接执行这些命令,而不创建模型消息。[命令 Agent Note](../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md) 负责分发与生命周期的决策依据;[包 README](../../packages/interaction/commands/README.md) 负责组合方式与限制。 + +来源:[`packages/interaction/commands/src/index.ts`](../../packages/interaction/commands/src/index.ts) + +## 输入元数据 + +该 seam 公开一个可选的非结构化输入提示。命令的可用性由插件组合决定:每个消费注册表的适配器都会看到全部生效定义。 + +```ts type-equiv +/** Immutable metadata for a command's optional unstructured input. */ +interface CommandInputDescriptor { + /** Placeholder shown before the user supplies free-form input. */ + readonly hint: string +} +``` + +## 定义 + +`CommandDefinition` 是由插件编写的注册定义。注册表会验证并冻结一份与原始注册对象脱离的生效定义。 + +```ts type-equiv +/** Plugin-owned command registration. */ +interface CommandDefinition { + /** Lowercase command name without the leading slash. */ + readonly name: string + /** Human-readable summary used in discovery UI. */ + readonly description: string + /** Optional free-form input hint advertised to capable clients. */ + readonly input?: CommandInputDescriptor + /** + * Whether `command/run` records `rawInput`. Defaults to true. A command + * whose domain event owns the payload sets this false to avoid duplicating + * that payload in the session log. + */ + readonly recordInput?: boolean + /** Execute against the receiving agent without sending the command to the model. */ + readonly handler: (invocation: CommandInvocation) => CommandResult | Promise +} +``` + +## 调用与结果 + +取消由适配器负责,适配器会传入确切的目标 agent。`rawInput` 紧接在解析后的名称之后,并保留适配器传入的分隔符与后缀。结果会直接呈现给 UI,而不是工具结果或会话事件。 + +```ts type-equiv +/** Invocation passed to one registered command handler. */ +interface CommandInvocation { + /** Exact agent whose human-facing surface received the command. */ + readonly agent: Agent + /** Exact text following the registered command name, including separator whitespace. */ + readonly rawInput: string + /** Cancellation signal owned by the dispatching UI request. */ + readonly signal: AbortSignal +} +``` + +```ts type-equiv +/** Expected command outcome rendered directly by the dispatching UI. */ +type CommandResult = + | { + readonly kind: 'success' + readonly text?: string + /** Earlier authoritative domain event that owns a richer presentation. */ + readonly sourceEventSeq?: number + } + | { readonly kind: 'error'; readonly text: string } +``` + +`sourceEventSeq` 是可选字段,且只用于成功结果。存在时,它指向接收会话日志中更早的一条非命令事件;`command/done` 会持久化同一引用,让客户端能够将命令生命周期与该领域投影合并,而无须解析 `text` 或依赖相邻行。 + +## 发现与解析视图 + +作用域解析后,适配器会获得不含处理器的不可变描述符。`parseCommand()` 在注册表解析前返回 `ParsedCommand`;语法有效的输入仍可能指向不可用的命令。 + +```ts type-equiv +/** Handler-free immutable command view returned to UI adapters. */ +interface CommandDescriptor { + /** Lowercase command name without the leading slash. */ + readonly name: string + /** Human-readable summary used in discovery UI. */ + readonly description: string + /** Optional free-form input hint advertised to capable clients. */ + readonly input?: CommandInputDescriptor +} +``` + +```ts type-equiv +/** Syntactically valid slash command before registry resolution. */ +interface ParsedCommand { + /** Lowercase command name without the leading slash. */ + readonly name: string + /** Exact text following the command name. */ + 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 new file mode 100644 index 0000000000..86348bad3a --- /dev/null +++ b/docs/subsystems/compaction.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/compaction.md +compaction.md: 5aef839192ea6f67bb409622187fe65fed795e73 +compaction.zh.md: 63b28518e68f71cc648d2f4e78a82bc5beafb621 diff --git a/docs/core-data-structures/compaction.md b/docs/subsystems/compaction.md similarity index 58% rename from docs/core-data-structures/compaction.md rename to docs/subsystems/compaction.md index f1df5b83bd..5aef839192 100644 --- a/docs/core-data-structures/compaction.md +++ b/docs/subsystems/compaction.md @@ -20,7 +20,7 @@ The lock brackets the **whole** operation: `compact/start` is appended first, th The markers are lock time points, not an exclusive container. An unrelated idle injection can appear between a standalone manual start and end while summarization is pending. The manual path revalidates only its selected positional span, so that injected context survives after the replacement checkpoint. A live unmatched start blocks every entry point; an unmatched start before a newer `session/end-seed` is stale evidence from a prior lifecycle and is ignored. -These variants are merged inside a `declare module '@deepseek-ai/dsh-session'` block, so — unlike the top-level types on the other sub-pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative shapes. +These variants are merged inside a `declare module '@deepseek-ai/dsh-session'` block, so — unlike the top-level types on the other subsystem pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative shapes. ## `CompactionResult` @@ -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](llm-streaming.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/core-data-structures/compaction.zh.md b/docs/subsystems/compaction.zh.md similarity index 57% rename from docs/core-data-structures/compaction.zh.md rename to docs/subsystems/compaction.zh.md index 52540250d4..63b28518e6 100644 --- a/docs/core-data-structures/compaction.zh.md +++ b/docs/subsystems/compaction.zh.md @@ -20,7 +20,7 @@ 这些标记表示锁的时间点,而不是排他的容器。摘要等待期间,不相关的空闲注入可以出现在独立的手动 start 与 end 之间。手动路径只重新验证所选位置 span,因此替换检查点之后仍保留该注入上下文。活动的未匹配 start 会阻塞所有入口点;较新 `session/end-seed` 之前的未匹配 start 是先前生命周期留下的陈旧证据,会被忽略。 -这些变体在 `declare module '@deepseek-ai/dsh-session'` 块内合并,因此——与其他子页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。 +这些变体在 `declare module '@deepseek-ai/dsh-session'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。 ## `CompactionResult` @@ -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](llm-streaming.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/packages/sdk/README.i18n.yaml b/docs/subsystems/core.i18n.yaml similarity index 57% rename from packages/sdk/README.i18n.yaml rename to docs/subsystems/core.i18n.yaml index 738a0610ab..4b3fdd41db 100644 --- a/packages/sdk/README.i18n.yaml +++ b/docs/subsystems/core.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 packages/sdk/README.md -README.md: 3f99d6d45ddc64ac068457b0d03e533d21451a5f -README.zh.md: 003383c5f6238b37893927813959d52d96350c3e +# pnpm run verify-translation-pairing --write docs/subsystems/core.md +core.md: 542147e1e0ac3608c8577bd2664bcf18736815b2 +core.zh.md: 388bb415f682fb541f2e4a2d4442651784fa27b3 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md new file mode 100644 index 0000000000..542147e1e0 --- /dev/null +++ b/docs/subsystems/core.md @@ -0,0 +1,943 @@ +# Core + +English | [中文](core.zh.md) + +The **core** subsystem is [`packages/core`](../../packages/core/README.md) — the control spine every composition boots: the event-sourced session log, system-prompt assembly, the tool registry, the agent vocabulary, and the concrete loop that drives them. This page owns what the `agent`/`agent-loop` pair declares — how an agent is created and owned, and the `Agent` handle with its delivery, cancellation, and interception contracts — plus the two type patterns every subsystem follows; the group's dedicated pages and the rest of the folder are indexed in the [subsystems README](README.md). + +## The spine, package by package + +A turn flows through the six packages in one loop: the driver in [`agent-loop`](../../packages/core/agent-loop) claims a queued prompt, opens a turn on the [session log](session.md) (`ctx.sessions`), assembles the request prefix through [system-prompt](system-prompt.md) (`ctx.systemPrompt`) and derives history from the log, streams the model response through the [LLM seam](llm-streaming.md), dispatches tool calls through the [tool registry](tools.md) (`ctx.tools`), and appends every model-visible fact back onto the log before the next step derives from it. The conversation vocabulary the loop moves — `Message`, `ContentBlock`, `StreamChunk`, the model request — is declared by [`packages/llm`](../../packages/llm/README.md) and documented on [llm-streaming.md](llm-streaming.md). + +| Package | Owns | Page | +|---|---|---| +| `session/` | The append-only `SessionEvent` log and in-memory store — the single source of truth (`ctx.sessions`) | [session.md](session.md) | +| `system-prompt/` | Prompt-section and tool-schema assembly (`ctx.systemPrompt`) | [system-prompt.md](system-prompt.md) | +| `tools/` | The scoped tool registry and guarded execution pipeline (`ctx.tools`) | [tools.md](tools.md) | +| `agent/` | The `Agent` interface, live registry, initiator scope, and `agent/*` event vocabulary (`ctx.agents`) | this page | +| `agent-loop/` | The concrete driver implementing the public `Agent` contract (`ctx.agentLoop`) | this page | +| `scope/` | The scoped-registration primitive the registries and loop build per-agent scoping on | [scope.md](scope.md) | + +`scope/` is the one non-service package: a dependency-free library (`createScope`/`scopeOf`/`scopeTarget`) that sits below `session/` and `system-prompt/` in the module graph precisely so they can consume it without a cycle. `agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop; it runs each driver inside `ctx.agents.withInitiator()`. Extension plugins depend on `agent` — including when they need the initiating Agent — and never on `agent-loop` directly, so the loop stays swappable. The default composition that wires this spine into a runnable agent is [`examples/agent-spine-demo`](../../packages/examples/agent-spine-demo/README.md). + +## Creation and ownership + +Consumers create agents through `ctx.agents` — `create()` builds a fresh session and agent under one caller-supplied `SessionId`, `resume()` loads a persisted session first — or declaratively through the loop's config entries. Programmatic creation returns the owner's handle: + +Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index.ts) + +```ts type-equiv +/** + * An owned agent plus its disposer, returned by {@link AgentRegistry.create} / + * {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers, + * only the holder can tear this agent down. The registered factory provider is + * also a structural owner because the scoped agent depends on that provider's + * service surface; provider unload stops and drains every live handle it made. + * `dispose()` stops the loop, awaits its exit, unregisters the agent, removes + * its session from the store, and finally unwinds its scoped world. + * + * `ctx.agents.get(id)` still returns a bare {@link Agent} — the handle is + * exposed only to the consumer owner that created it; the structural provider + * reaches the same teardown internally. Config-created agents (the loop's own + * startup) are owned by the loop fiber and never need a handle. + */ +interface AgentHandle { + agent: Agent + dispose(): Promise +} +``` + +`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, seed boundary, origin classification, delegation depth), an optional `seed` replay prefix for forks, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) composes the agent's scoped world while both ids are still unpublished — everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly — and may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal rolls the transaction back without publishing either id. + +`AgentFactory` is the creation seam behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers program against `ctx.agents` without depending on the concrete loop package. The exact `create`/`resume` signatures and their rollback contracts are in the [generated section](#ctxagents--agentregistry) below. + +## The agent handle + +`Agent` is the surface every plugin (UI, hooks, orchestrators) programs against; `ctx.agents.get(id)` returns it, and the [initiator scope](#initiating-agent) carries it. The concrete implementation is package-internal to dsh-agent-loop; nothing outside the loop depends on it. The unified `send` method exposes target and wakeup routing directly; `followup`, `steer`, and `inject` are fixed-preset aliases. + +Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) + +```ts type-equiv +/** Public live-agent handle. */ +interface Agent { + /** The single identity shared with {@link session}. */ + readonly id: SessionId + /** The provider route and model this agent's requests use. */ + readonly options: AgentOptions + /** The live session this agent drives; its log is the durable source of truth. */ + readonly session: Session + /** The agent-owned projection of durable pending work. */ + readonly inbox: Inbox + /** The current lifecycle state, mirrored on every `agent/status` transition. */ + readonly status: AgentStatus + /** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */ + readonly ctx: Context + + /** + * Clear queued and steering work — unless `keepInbox` — and abort the active + * turn or between-turn task. The first cause wins for that activity. With no + * active activity, cancellation is a no-op and does not arm later work. + * @param cause - the stable caller intent carried by the active operation signal. + * @param options - cancellation options; `keepInbox` preserves pending work. + */ + cancel(cause: AgentCancelCause, options?: CancelOptions): void + + /** + * Resolve after the current whole-agent activity reaches quiescence. This + * follows replacement work started before the observed driver retires, + * but does not identify the settlement of any particular message. + * @returns fulfillment after no active driver or maintenance task remains. + */ + whenIdle(): Promise + + /** + * Run one non-turn maintenance task from the true idle phase. The task starts + * synchronously after claiming that phase; later waking input remains in the + * inbox until the task settles, while public status stays `idle`. + * `whenIdle()` follows both the task and any waking work released behind it. + * @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}. + * @throws synchronously when turn-driving or another maintenance task already owns the agent. + * @returns the task promise. + */ + runMaintenance(task: (signal: AbortSignal) => Promise): Promise + + /** + * Route identified input to an inbox boundary and optionally wake the driver. + * Waking input submitted after active cancellation is queued for the next + * turn and runs when the aborted activity converges to idle; a `disposed` + * cancel leaves it parked. A wake submitted while already idle always opens + * its turn boundary, even when its message is cleared before the driver + * claims ([cancel-convergence wake latch](../../../../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)). + * @param message - identified content and its producer provenance. + * @param target - the preferred next-turn or next-step inbox boundary. + * @param wakeup - whether delivery may wake the driver. + */ + send(message: UserMessage, target: InboxTarget, wakeup: boolean): void + + /** + * Queue an ordinary follow-up turn and wake the driver. The item becomes the + * sole ordinary message of its own turn. + * @param message - identified prompt content and its producer provenance. + */ + followup(message: UserMessage): void + + /** + * Submit steering for the nearest step. An idle driver starts a turn; + * a running driver consumes it at its next step boundary. + * A rejected step leaves steering parked in the inbox until the next + * wake; cancellation or disposal may discard pending steering. + * @param message - identified steering content and its producer provenance. + */ + steer(message: UserMessage): void + + /** + * Queue model-facing context for the next pre-step without waking the + * driver. A running driver claims it at the nearest later step boundary; + * idle drivers leave it pending until follow-up or steering + * wakes them. It may miss a request whose pre-step already claimed its + * batch. Cancellation or disposal may discard pending context. + * @param message - identified injected context and its producer provenance. + */ + inject(message: UserMessage): void +} +``` + +```ts type-equiv +/** + * An agent's lifecycle state, emitted on every transition as `agent/status`: + * `idle` means no driver is active; `running` begins when waking input starts + * cancellable pre-step processing and lasts while the driver drains, + * closes, or checkpoints turns. Disposal removes the agent from its registry; + * it is not a third observable status. + */ +type AgentStatus = 'idle' | 'running' +``` + +`running` describes the driver-wide drain interval and may span consecutive queued turns; it does not prove a turn is still open. Disposal removes the agent from the registry and emits `agent/disposed`; it is not a terminal status value. `followup()` returns no handle: its `MessageId` identifies durable inbox insertion, claim, and discard facts, not a later assistant output or turn ending. `whenIdle()` observes the whole agent, so callers may call a receipt-to-idle interval a run only when they explicitly own that interval ([decision](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md)). + +```ts type-equiv +/** Merge-extensible agent creation options. Persona belongs to system-prompt sections. */ +interface AgentOptions { + /** Provider route (must have a registered adapter at call time). */ + provider?: string + /** Model id interpreted by the selected provider adapter. */ + model?: string + /** Maximum output tokens for each conversation-model request. */ + maxTokens?: number +} +``` + +Dispatch requires `provider` and `model` after `agent/request`. When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. An agent-scoped `deployment:persona` prompt section may shadow the global default persona. + +The inbox is the delivery vocabulary — two ordered pending-message lists the agent owns as a durable projection: + +```ts type-equiv +/** One of the two ordered pending-message lists owned by an agent. */ +type InboxTarget = 'next-turn' | 'next-step' +``` + +Every pending occurrence is its `UserMessage`; `MessageId` is the sole identity. `Inbox.append`, `prepend`, `replace`, `remove`, `clear`, `splice`, and `claim` record normalized durable `agent/inbox/spliced` mutations and reject duplicate pending ids. `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists; replacement may change identity and emits the old message as discarded followed by the new message as inserted. Ordinary removals and `clear()` are cancellations. `claim(target)` removes the proposed step batch — all `next-step` input plus, at a turn boundary, one `next-turn` message — through pure deletion splices without emitting discarded notifications, and the loop separately emits per-message claimed notifications. Whole-queue consumers such as UI projections reconstruct `nextTurn` and `nextStep` from the durable splices, while consumers following one message use the exact `agent/inbox/inserted`, `claimed`, and `discarded` notifications. + +Cancellation: + +```ts type-equiv +/** Options for {@link Agent.cancel}. */ +interface CancelOptions { + /** + * Preserve queued and steering inbox items instead of discarding them. The + * active turn is still aborted, but un-started and pending work survives for a + * later turn and no canceled inbox splice is logged. + */ + keepInbox?: boolean | undefined +} +``` + +```ts type-equiv +/** Why an active agent driver was cancelled. */ +type AgentCancelCause = + | { readonly kind: 'user' } + | { readonly kind: 'parent' } + | { readonly kind: 'hook'; readonly reason: string } + | { readonly kind: 'disposed' } +``` + +The cause is a TypeScript-enforced same-process input. An active cancellation holder copies it into the runtime-only `AbortSignal.reason`; a signal grants cooperating listeners no classification authority. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result. + +The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits. + +## Initiating Agent + +The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns its lifetime and boundary rules. + +## Interception decisions + +Pre-step decisions use the same identified `UserMessage` shape as durable user-role input. The entered batch is authoritative and preserves every message's identity and provenance. Hook bridges map their native decision fields onto this typed result. + +Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) + +`agent/pre-step` receives one payload carrying the exclusive claimed batch (`messages`), the proposed step's coordinates (`turn`, `step`), and the current turn's cancellation `signal`. The initial proposal runs inside an open turn before any step; a tool continuation may submit an empty claimed batch between steps: + +It returns a `PreStepDecision`. Reject opens no step. Enter supplies the complete message batch appended after `step/start`; claimed messages omitted by the final decision remain removed, while input inserted after the claim stays pending: + +```ts type-equiv +/** Whether and with which messages the loop enters a proposed step. */ +type PreStepDecision = + | { kind: 'reject' } + | { kind: 'enter'; messages: UserMessage[] } +``` + +`agent/request-error` runs after a failed model step closes and before its turn closes. Listeners can repair durable state or await policy work while the failed turn's signal is still live. A handling listener returns `{ kind: 'retry' }` without calling `next()`; the default `undefined` leaves the failure terminal. + +```ts type-equiv +/** Action returned by a listener that owns model-request recovery. */ +type RequestErrorAction = { kind: 'retry' } | undefined +``` + +`agent/pre-step` is the single serial boundary before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain. + +`agent/session-start` carries a `SessionStartSource` (why the session lifecycle began; a bridge keys its SessionStart matcher on it): + +```ts type-equiv +/** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */ +type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact' +``` + +## Sessions + +A `Session` is an **append-only log** of typed `SessionEvent`s — the single source of truth. The LLM message history is *derived* from the log (`deriveMessages()`), not stored separately. Every entry carries a monotonic `seq`, a `time`, and a `type`-discriminated `data` payload; surface variants additionally carry `sourceEventSeqs` provenance and a `surfaceOp`. + +The `SessionEvent` envelope's exact conditional shape, the twelve event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/message`, `assistant/chunk`, `assistant/message`, `tool/call`, `tool/result`, `steering/message`, `todo/write`, `request/header`), the `deriveMessages()` projection rules, the `TurnTrigger`/`TurnEndReason` reasons, and the execution-enclosure and standalone-event rules are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` seam, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**. + +## `ToolDefinition` + +The one pipeline-authoring type that is core: what every registered tool *is* — a model-facing `ToolSchema` plus an `execute` function and optional final-content and UI callbacks. A tool author rarely constructs it by hand (the `defineTool` DSL builds it with typed args), but it is the contract the registry holds and the loop dispatches through. + +Its full fields, the `defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` typed schema DSL, the `ToolExecution`/`ToolExecutionResult` waterfall shapes, and the tool-presentation UI vocabulary are on **[tools.md](tools.md)**. + +## Repo-wide type patterns + +Two patterns recur across every subsystem and are documented once, here. + +### The `…Map → derived-union` pattern + +Almost every extensible sum type in the harness follows one shape: an interface keyed by a discriminant tag (the `…Map`), from which the union is derived with `keyof`. Plugins add variants by **declaration merging** — no edit to the owning package. + +```ts ignore-check +// The pattern, schematically: +interface ThingMap { + 'a': { kind: 'a'; /* … */ } + 'b': { kind: 'b'; /* … */ } +} +type ThingKind = keyof ThingMap // 'a' | 'b' +type Thing = ThingMap[keyof ThingMap] // the discriminated union + +// A plugin extends it without touching the source package: +declare module '@deepseek-ai/dsh-llm' { + interface ThingMap { + 'c': { kind: 'c'; /* … */ } + } +} +``` + +Six canonical maps use this pattern; a plugin author extends these: + +| Map | Package | Derives | Catalog | +|---|---|---|---| +| `ContentBlockMap` | dsh-llm | `ContentBlock` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) | +| `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) | +| `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.md#the-model-request-and-result) | +| `TurnTriggerMap` | dsh-session | `TurnTrigger` | [session.md](session.md) | +| `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.md) | +| `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.md) | + +Two large discriminated unions are the ones consumers `switch` over most: **`StreamChunk`** (the streaming protocol) and **`SessionEvent`** (the log entry). Per the repo convention, `switch` on the tag — don't chain `if`s — so each arm narrows and a typo'd tag fails to compile. + +### Branded IDs + +IDs that cross package boundaries are **branded** — structurally strings, but non-interchangeable at the type level (a `SessionId` cannot be passed where a `CallId` is expected). Construction goes through a per-type factory; comparison, logging, and JSON behave as ordinary strings. + +The `Branded` primitive lives in its own type-only package, [dsh-brand](../../packages/util/brand) (no runtime code, no harness-package dependency), so any package can brand the ids it owns without depending on an unrelated capability package. + +Source: [`packages/util/brand/src/index.ts`](../../packages/util/brand/src/index.ts) + +```ts type-equiv +/** A string carrying a compile-time-only brand `B`. */ +type Branded = string & { readonly [BRAND]: B } +``` + +The two core IDs are `CallId` (correlates a tool call with its result; dsh-llm) and `SessionId` (the shared live agent and durable session identity; dsh-session). Capability packages brand their own ids too, such as `TaskId` in [tasks.md](tasks.md). + + + + + +## 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.agentPresets` — `AgentPresets` + +Registry over the deployment's agent presets. + +Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a preset authored while the process runs is visible immediately, and a preset deleted underneath a picker disappears from the next read. + +```ts cordis-catalog +/** + * Every preset the configured roots currently supply. + * @returns the presets, first-root-wins per id. + */ +async list(): Promise + +/** + * Resolve one preset by id. + * @param id - the preset id, or `undefined` for {@link defaultId}. + * @returns the resolved preset. + * @throws when no configured root supplies that id. + */ +async resolve(id?: string): Promise + +/** + * Compose one agent from a preset: ensure the preset's standing mount, then + * parent the agent's scope key to it so the mount's registrations and + * listeners cover this agent. + * + * Call from the agent factory's `setup(agentCtx)`; a rejection there rolls + * the agent creation back, so a broken preset never yields a half-composed + * session. + * @param agentCtx - the agent's scope context. + * @param id - the preset id, or `undefined` for {@link defaultId}. + * @returns the preset that was composed, for the caller to record. + * @throws when the preset is unknown or its composition is unusable. + */ +async mount(agentCtx: Context, id?: string): Promise + +/** + * One agent's instance of a service its preset mounted. + * + * A preset publishes services behind `isolate` realms, which are invisible + * outside the group that declares them — including to the host. This is how a + * caller holding the agent reads one anyway: a request that is ABOUT a + * session but arrives from outside it, which is every browser RPC. + * + * Read addressing only. A host row that `inject`s a service cannot use this, + * because injection resolves before any session exists and has no agent to + * key by; such a service belongs on the host plane instead. + * @param agent - the agent whose composition to look inside. + * @param name - the service name as the preset's rows resolve it. + * @returns the agent's instance, or undefined when its preset mounts none. + */ +serviceFor(agent: { ctx: Context }, name: K): Context[K] | undefined + +/** + * Re-link one agent to a different preset's standing composition. + * + * Only valid while the agent has produced nothing: swapping tools mid + * conversation would leave logged tool calls the new composition cannot + * make. The CALLER owns that check — this method does not read session + * history. + * + * The swap is a parent re-link, not an unmount: standing mounts are shared + * and permanent, so the old composition stays for its other agents and the + * new one is ensured BEFORE the link moves. An unknown or unusable preset + * therefore throws with the agent exactly as it was — there is no torn-down + * state to restore. + * @param agentCtx - the agent's scope context. + * @param id - the preset to compose the agent from instead. + * @returns the preset now installed. + * @throws when the preset is unknown or its composition is unusable. + */ +async recompose(agentCtx: Context, id: string): Promise + +/** + * The standing scope key of one preset, for a host reader with no agent. + * + * A cold transcript read resolves tool presenters against the composition + * the session recorded, and the standing mount makes that possible without + * resuming anything: ensuring the mount composes plugins but starts no + * agent, no session, and no turn. + * @param id - the preset id, or `undefined` for {@link defaultId}. + * @returns the standing scope key readers pass as a registry view scope. + * @throws when the preset is unknown or its composition is unusable. + */ +async standingKeyFor(id?: string): Promise +``` + +Types: [ScopeKey](scope.md) + +Source: [`packages/preset/agent-presets/src/index.ts:68`](../../packages/preset/agent-presets/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:254`](../../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: [LlmCallConfig](llm-streaming.md) · [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 new file mode 100644 index 0000000000..388bb415f6 --- /dev/null +++ b/docs/subsystems/core.zh.md @@ -0,0 +1,951 @@ +# 核心 + +[English](core.md) | 中文 + +**核心**子系统即 [`packages/core`](../../packages/core/README.md)——每个组合都会启动的控制主干:事件溯源的会话日志、系统提示词组装、工具注册表、agent 词汇,以及驱动它们的具体循环。本页拥有 `agent`/`agent-loop` 这对包所声明的内容——agent 如何被创建与拥有,以及 `Agent` 句柄及其投递、取消与拦截契约——外加每个子系统都遵循的两个类型模式;该组的专属页面与目录其余部分见[子系统 README](README.md)。 + +## 主干逐包速览 + +一个轮次按同一条循环流经六个包:[`agent-loop`](../../packages/core/agent-loop) 中的 driver 认领一条排队的 prompt,在[会话日志](session.md)(`ctx.sessions`)上开启轮次,通过 [system-prompt](system-prompt.md)(`ctx.systemPrompt`)组装请求前缀并从日志派生历史,经 [LLM seam](llm-streaming.md) 流式获取模型响应,经[工具注册表](tools.md)(`ctx.tools`)分发工具调用,并把每个模型可见的事实追加回日志,供下一步派生。循环搬运的对话词汇——`Message`、`ContentBlock`、`StreamChunk`、模型请求——由 [`packages/llm`](../../packages/llm/README.md) 声明,记录在 [llm-streaming.md](llm-streaming.md)。 + +| 包 | 负责内容 | 页面 | +|---|---|---| +| `session/` | 只追加的 `SessionEvent` 日志与内存 store——唯一事实来源(`ctx.sessions`) | [session.md](session.md) | +| `system-prompt/` | 提示词段落与工具 schema 组装(`ctx.systemPrompt`) | [system-prompt.md](system-prompt.md) | +| `tools/` | 带作用域的工具注册表与受保护的执行流水线(`ctx.tools`) | [tools.md](tools.md) | +| `agent/` | `Agent` 接口、实时注册表、发起者作用域与 `agent/*` 事件词汇(`ctx.agents`) | 本页 | +| `agent-loop/` | 实现公开 `Agent` 契约的具体 driver(`ctx.agentLoop`) | 本页 | +| `scope/` | 注册表与循环用于构建按 agent 作用域的注册原语 | [scope.md](scope.md) | + +`scope/` 是这里唯一的非服务包:一个零依赖库(`createScope`/`scopeOf`/`scopeTarget`),在模块图中位于 `session/` 与 `system-prompt/` 之下,正是为了让它们消费它而不形成环。`agent-loop` 是 `agent` seam 的唯一具体实现,放在这里因为它是 harness 的默认产品循环;它在 `ctx.agents.withInitiator()` 内运行每个 driver。扩展插件依赖 `agent`——包括需要发起 Agent 时——而绝不直接依赖 `agent-loop`,因此循环保持可替换。把这条主干接成可运行 agent 的默认组合是 [`examples/agent-spine-demo`](../../packages/examples/agent-spine-demo/README.md)。 + + + +## 创建与所有权 + +消费方通过 `ctx.agents` 创建 agent——`create()` 在一个调用方提供的 `SessionId` 下构建全新会话与 agent,`resume()` 先加载持久会话——或者通过循环的声明式配置条目创建。编程式创建返回归属所有者的句柄: + +源码:[`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index.ts) + +```ts type-equiv +/** + * An owned agent plus its disposer, returned by {@link AgentRegistry.create} / + * {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers, + * only the holder can tear this agent down. The registered factory provider is + * also a structural owner because the scoped agent depends on that provider's + * service surface; provider unload stops and drains every live handle it made. + * `dispose()` stops the loop, awaits its exit, unregisters the agent, removes + * its session from the store, and finally unwinds its scoped world. + * + * `ctx.agents.get(id)` still returns a bare {@link Agent} — the handle is + * exposed only to the consumer owner that created it; the structural provider + * reaches the same teardown internally. Config-created agents (the loop's own + * startup) are owned by the loop fiber and never need a handle. + */ +interface AgentHandle { + agent: Agent + dispose(): Promise +} +``` + +`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、seed 边界、来源分类、委派深度)、fork 用的可选 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应物:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在两个 id 都尚未发布时组装 agent 的作用域世界——凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在——并可返回一个在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose 都会回滚事务,两个 id 均不发布。 + +`AgentFactory` 是注册表背后的创建 seam:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方面向 `ctx.agents` 编程,无需依赖具体循环包。确切的 `create`/`resume` 签名及其回滚契约见下方[生成区块](#ctxagents--agentregistry)。 + + + +## Agent 句柄 + +`Agent` 是每个插件(UI、钩子、orchestrator)面向编程的 surface;`ctx.agents.get(id)` 返回它,[发起者作用域](#initiating-agent)携带它。具体实现为 dsh-agent-loop 包内部细节;循环外没有任何组件依赖它。统一的 `send` 方法直接暴露 target 与 wakeup 路由;`followup`、`steer` 与 `inject` 是固定预设的别名方法。 + +源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) + +```ts type-equiv +/** Public live-agent handle. */ +interface Agent { + /** The single identity shared with {@link session}. */ + readonly id: SessionId + /** The provider route and model this agent's requests use. */ + readonly options: AgentOptions + /** The live session this agent drives; its log is the durable source of truth. */ + readonly session: Session + /** The agent-owned projection of durable pending work. */ + readonly inbox: Inbox + /** The current lifecycle state, mirrored on every `agent/status` transition. */ + readonly status: AgentStatus + /** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */ + readonly ctx: Context + + /** + * Clear queued and steering work — unless `keepInbox` — and abort the active + * turn or between-turn task. The first cause wins for that activity. With no + * active activity, cancellation is a no-op and does not arm later work. + * @param cause - the stable caller intent carried by the active operation signal. + * @param options - cancellation options; `keepInbox` preserves pending work. + */ + cancel(cause: AgentCancelCause, options?: CancelOptions): void + + /** + * Resolve after the current whole-agent activity reaches quiescence. This + * follows replacement work started before the observed driver retires, + * but does not identify the settlement of any particular message. + * @returns fulfillment after no active driver or maintenance task remains. + */ + whenIdle(): Promise + + /** + * Run one non-turn maintenance task from the true idle phase. The task starts + * synchronously after claiming that phase; later waking input remains in the + * inbox until the task settles, while public status stays `idle`. + * `whenIdle()` follows both the task and any waking work released behind it. + * @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}. + * @throws synchronously when turn-driving or another maintenance task already owns the agent. + * @returns the task promise. + */ + runMaintenance(task: (signal: AbortSignal) => Promise): Promise + + /** + * Route identified input to an inbox boundary and optionally wake the driver. + * Waking input submitted after active cancellation is queued for the next + * turn and runs when the aborted activity converges to idle; a `disposed` + * cancel leaves it parked. A wake submitted while already idle always opens + * its turn boundary, even when its message is cleared before the driver + * claims ([cancel-convergence wake latch](../../../../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)). + * @param message - identified content and its producer provenance. + * @param target - the preferred next-turn or next-step inbox boundary. + * @param wakeup - whether delivery may wake the driver. + */ + send(message: UserMessage, target: InboxTarget, wakeup: boolean): void + + /** + * Queue an ordinary follow-up turn and wake the driver. The item becomes the + * sole ordinary message of its own turn. + * @param message - identified prompt content and its producer provenance. + */ + followup(message: UserMessage): void + + /** + * Submit steering for the nearest step. An idle driver starts a turn; + * a running driver consumes it at its next step boundary. + * A rejected step leaves steering parked in the inbox until the next + * wake; cancellation or disposal may discard pending steering. + * @param message - identified steering content and its producer provenance. + */ + steer(message: UserMessage): void + + /** + * Queue model-facing context for the next pre-step without waking the + * driver. A running driver claims it at the nearest later step boundary; + * idle drivers leave it pending until follow-up or steering + * wakes them. It may miss a request whose pre-step already claimed its + * batch. Cancellation or disposal may discard pending context. + * @param message - identified injected context and its producer provenance. + */ + inject(message: UserMessage): void +} +``` + +```ts type-equiv +/** + * An agent's lifecycle state, emitted on every transition as `agent/status`: + * `idle` means no driver is active; `running` begins when waking input starts + * cancellable pre-step processing and lasts while the driver drains, + * closes, or checkpoints turns. Disposal removes the agent from its registry; + * it is not a third observable status. + */ +type AgentStatus = 'idle' | 'running' +``` + +`running` 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。dispose(资源释放)会把 agent 从注册表移除并发出 `agent/disposed`;它不是一个终态 status 值。`followup()` 不返回句柄:其 `MessageId` 标识的是持久的 inbox 插入、认领与丢弃事实,而非之后的助手输出或轮次结束。`whenIdle()` 观察的是整个 agent,因此只有当调用方明确拥有从回执到空闲的这段区间时,才能把它称为一次 run([决策](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md))。 + +```ts type-equiv +/** Merge-extensible agent creation options. Persona belongs to system-prompt sections. */ +interface AgentOptions { + /** Provider route (must have a registered adapter at call time). */ + provider?: string + /** Model id interpreted by the selected provider adapter. */ + model?: string + /** Maximum output tokens for each conversation-model request. */ + maxTokens?: number +} +``` + +在 `agent/request` 之后,分发要求 `provider` 与 `model` 都存在。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。agent 作用域的 `deployment:persona` 提示词段落可以遮蔽全局默认 persona。 + +inbox 即投递词汇——agent 以持久投影形式拥有的两条有序待处理消息列表: + +```ts type-equiv +/** One of the two ordered pending-message lists owned by an agent. */ +type InboxTarget = 'next-turn' | 'next-step' +``` + +每个待处理入队项就是其 `UserMessage`;`MessageId` 是唯一标识。`Inbox.append`、`prepend`、`replace`、`remove`、`clear`、`splice` 与 `claim` 会记录规范化的持久 `agent/inbox/spliced` 变更,并拒绝重复的待处理 id。`replace(messageId, newMessage)` 与 `remove(messageId)` 通过 `MessageId` 跨两份列表定位待处理消息;替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。普通删除和 `clear()` 都表示取消。`claim(target)` 通过无 outcome 的纯删除 splice 移除拟进入步骤的批次——全部 `next-step` 输入,外加轮次边界上的一条 `next-turn` 消息——且不发出 discarded 通知;循环另行逐条发出 claimed 通知。UI 投影等整体队列消费方通过持久 splice 重建 `nextTurn` 与 `nextStep`,而跟踪单条消息的消费方使用精确的 `agent/inbox/inserted`、`claimed` 与 `discarded` 通知。 + +取消: + +```ts type-equiv +/** Options for {@link Agent.cancel}. */ +interface CancelOptions { + /** + * Preserve queued and steering inbox items instead of discarding them. The + * active turn is still aborted, but un-started and pending work survives for a + * later turn and no canceled inbox splice is logged. + */ + keepInbox?: boolean | undefined +} +``` + +```ts type-equiv +/** Why an active agent driver was cancelled. */ +type AgentCancelCause = + | { readonly kind: 'user' } + | { readonly kind: 'parent' } + | { readonly kind: 'hook'; readonly reason: string } + | { readonly kind: 'disposed' } +``` + +cause 是由 TypeScript 强制约束的同进程输入。活跃的取消持有者会将它复制到仅运行时的 `AbortSignal.reason`;signal 不授予协作监听器任何分类权限。持久 `turn/end` 保留粗粒度 `{ kind: 'aborted' }` 结果;若需记录请求 provenance,应使用单独的持久事件,而不是让终态结果承担额外含义。 + +[事件分类](../architecture.md#event)拥有 `agent/*` 生命周期、检查点与 waterfall(瀑布式事件)契约。轮次和步骤边界是持久会话事件,而不是 agent emit。 + + + +## 发起 Agent + +`ctx.agents` 携带的进程本地 initiator 就是上面的确切 `Agent`,不是单独的 frame 或复制的标识。环境中存在该值既不能证明存活,也不代表授权;其生命周期与边界规则由 [initiator 作用域决策](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)规定。 + +## 拦截决策 + +pre-step 决策使用与持久 user-role 输入相同、带标识的 `UserMessage` 形状。进入步骤的批次具有权威性,并保留每条消息的标识与 provenance。钩子桥接层把其原生决策字段映射到这一类型化结果上。 + +源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) + +`agent/pre-step` 接收一个 payload,携带独占的已领取批次(`messages`)、拟进入步骤的坐标(`turn`、`step`)与当前轮次的取消 `signal`。首次提案在已打开的轮次内、任何步骤开始前运行;工具 continuation 可以在步骤之间提交空的已领取批次: + +它返回 `PreStepDecision`。reject 不会打开步骤。enter 提供在 `step/start` 后追加的完整消息批次;最终决策省略的已领取消息保持已删除,而领取后插入的输入仍留待后续处理: + +```ts type-equiv +/** Whether and with which messages the loop enters a proposed step. */ +type PreStepDecision = + | { kind: 'reject' } + | { kind: 'enter'; messages: UserMessage[] } +``` + +`agent/request-error` 在失败的模型步骤关闭之后、其轮次关闭之前运行。listener 可以在失败轮次的 signal 仍然存活时修复持久状态或 await 策略工作。处理该错误的 listener 返回 `{ kind: 'retry' }` 且不调用 `next()`;默认的 `undefined` 会让失败保持终态。 + +```ts type-equiv +/** Action returned by a listener that owns model-request recovery. */ +type RequestErrorAction = { kind: 'retry' } | undefined +``` + +`agent/pre-step` 是请求推导前唯一的串行边界。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。 + +`agent/session-start` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart): + +```ts type-equiv +/** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */ +type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact' +``` + +## 会话 + +`Session` 是一份类型化 `SessionEvent` 的**仅追加日志**——唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`),而非单独存储。每个条目携带单调的 `seq`、`time` 与按 `type` 判别的 `data` payload;surface 变体还额外携带 `sourceEventSeqs` provenance 与 `surfaceOp`。 + +`SessionEvent` 信封的确切条件形状、十二种事件变体(`turn/start`、`turn/end`、`step/start`、`step/end`、`user/message`、`assistant/chunk`、`assistant/message`、`tool/call`、`tool/result`、`steering/message`、`todo/write`、`request/header`)、`deriveMessages()` 投影规则、`TurnTrigger`/`TurnEndReason` 原因以及执行封闭和独立事件规则都在 **[session.md](session.md)** 中。日志如何持久化——`SessionPersistence` seam、JSONL/SQLite 后端、`session/flush` 检查点、崩溃恢复与 `SessionHeader`——则在 **[persistence.md](persistence.md)** 中。 + +## `ToolDefinition` + +唯一属于核心的流水线编写类型:每个已注册工具*是什么*——一个面向模型的 `ToolSchema` 加上一个 `execute` 函数,以及可选的最终内容回调与 UI 回调。工具作者很少手动构造它(`defineTool` DSL 会用类型化参数构建),但它是注册表持有、循环分发所经过的契约。 + +其完整字段、`defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` 类型化 schema DSL、`ToolExecution`/`ToolExecutionResult` waterfall 形状,以及工具展示 UI 词汇在 **[tools.md](tools.md)** 中。 + +## 全仓通用类型模式 + +两个模式在每个子系统中反复出现,只在此处记录一次。 + +### `…Map → derived-union` 模式 + +harness 中几乎所有可扩展的和类型都遵循同一形状:一个以判别标签为键的接口(`…Map`),联合类型由 `keyof` 派生。插件通过**声明合并**添加变体——无需修改拥有该类型的包(package)。 + +```ts ignore-check +// The pattern, schematically: +interface ThingMap { + 'a': { kind: 'a'; /* … */ } + 'b': { kind: 'b'; /* … */ } +} +type ThingKind = keyof ThingMap // 'a' | 'b' +type Thing = ThingMap[keyof ThingMap] // the discriminated union + +// A plugin extends it without touching the source package: +declare module '@deepseek-ai/dsh-llm' { + interface ThingMap { + 'c': { kind: 'c'; /* … */ } + } +} +``` + +六个规范 map 使用此模式;插件作者扩展它们: + +| Map | 包 | 派生 | 目录 | +|---|---|---|---| +| `ContentBlockMap` | dsh-llm | `ContentBlock` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) | +| `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) | +| `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.md#the-model-request-and-result) | +| `TurnTriggerMap` | dsh-session | `TurnTrigger` | [session.md](session.md) | +| `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.md) | +| `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.md) | + +消费方最常 `switch` 的两个大型判别联合类型是:**`StreamChunk`**(流式协议)和 **`SessionEvent`**(日志条目)。按仓库约定,对标签做 `switch`——不要链式 `if`——这样每个分支都能窄化类型,拼错的标签会编译失败。 + + + +### 品牌化 ID + +跨越包边界的 ID 都经过**品牌化**——结构上是字符串,但在类型层面不可互换(不能把 `SessionId` 传给需要 `CallId` 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。 + +`Branded` 原语位于独立的纯类型包 [dsh-brand](../../packages/util/brand) 中(没有运行时代码,也不依赖 Harness 包),因此任何包都能品牌化其拥有的 id,而无需依赖无关的能力包。 + +源码:[`packages/util/brand/src/index.ts`](../../packages/util/brand/src/index.ts) + +```ts type-equiv +/** A string carrying a compile-time-only brand `B`. */ +type Branded = string & { readonly [BRAND]: B } +``` + +两个核心 ID 是 `CallId`(关联工具调用及其结果;dsh-llm)和 `SessionId`(活跃 agent 与持久会话共享的标识;dsh-session)。能力包也会品牌化各自的 id,例如 [tasks.md](tasks.md) 中的 `TaskId`。 + + + + + +## 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.agentPresets` — `AgentPresets` + +Registry over the deployment's agent presets. + +Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a preset authored while the process runs is visible immediately, and a preset deleted underneath a picker disappears from the next read. + +```ts cordis-catalog +/** + * Every preset the configured roots currently supply. + * @returns the presets, first-root-wins per id. + */ +async list(): Promise + +/** + * Resolve one preset by id. + * @param id - the preset id, or `undefined` for {@link defaultId}. + * @returns the resolved preset. + * @throws when no configured root supplies that id. + */ +async resolve(id?: string): Promise + +/** + * Compose one agent from a preset: ensure the preset's standing mount, then + * parent the agent's scope key to it so the mount's registrations and + * listeners cover this agent. + * + * Call from the agent factory's `setup(agentCtx)`; a rejection there rolls + * the agent creation back, so a broken preset never yields a half-composed + * session. + * @param agentCtx - the agent's scope context. + * @param id - the preset id, or `undefined` for {@link defaultId}. + * @returns the preset that was composed, for the caller to record. + * @throws when the preset is unknown or its composition is unusable. + */ +async mount(agentCtx: Context, id?: string): Promise + +/** + * One agent's instance of a service its preset mounted. + * + * A preset publishes services behind `isolate` realms, which are invisible + * outside the group that declares them — including to the host. This is how a + * caller holding the agent reads one anyway: a request that is ABOUT a + * session but arrives from outside it, which is every browser RPC. + * + * Read addressing only. A host row that `inject`s a service cannot use this, + * because injection resolves before any session exists and has no agent to + * key by; such a service belongs on the host plane instead. + * @param agent - the agent whose composition to look inside. + * @param name - the service name as the preset's rows resolve it. + * @returns the agent's instance, or undefined when its preset mounts none. + */ +serviceFor(agent: { ctx: Context }, name: K): Context[K] | undefined + +/** + * Re-link one agent to a different preset's standing composition. + * + * Only valid while the agent has produced nothing: swapping tools mid + * conversation would leave logged tool calls the new composition cannot + * make. The CALLER owns that check — this method does not read session + * history. + * + * The swap is a parent re-link, not an unmount: standing mounts are shared + * and permanent, so the old composition stays for its other agents and the + * new one is ensured BEFORE the link moves. An unknown or unusable preset + * therefore throws with the agent exactly as it was — there is no torn-down + * state to restore. + * @param agentCtx - the agent's scope context. + * @param id - the preset to compose the agent from instead. + * @returns the preset now installed. + * @throws when the preset is unknown or its composition is unusable. + */ +async recompose(agentCtx: Context, id: string): Promise + +/** + * The standing scope key of one preset, for a host reader with no agent. + * + * A cold transcript read resolves tool presenters against the composition + * the session recorded, and the standing mount makes that possible without + * resuming anything: ensuring the mount composes plugins but starts no + * agent, no session, and no turn. + * @param id - the preset id, or `undefined` for {@link defaultId}. + * @returns the standing scope key readers pass as a registry view scope. + * @throws when the preset is unknown or its composition is unusable. + */ +async standingKeyFor(id?: string): Promise +``` + +Types: [ScopeKey](scope.md) + +Source: [`packages/preset/agent-presets/src/index.ts:68`](../../packages/preset/agent-presets/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:254`](../../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: [LlmCallConfig](llm-streaming.md) · [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 new file mode 100644 index 0000000000..7028d71f8d --- /dev/null +++ b/docs/subsystems/credentials.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/credentials.md +credentials.md: 0bc2224ac039addc795d3806e8c85004f2bb84a7 +credentials.zh.md: 6f5ccd1572885434257d39eb0f659fcc7371ad1d diff --git a/docs/subsystems/credentials.md b/docs/subsystems/credentials.md new file mode 100644 index 0000000000..0bc2224ac0 --- /dev/null +++ b/docs/subsystems/credentials.md @@ -0,0 +1,133 @@ +# User Credentials + +English | [中文](credentials.zh.md) + +The credential seam of [dsh-credentials](../../packages/credentials/credentials) keeps secrets out of configuration: settings sections and `cordis.yml` entries carry *references* (environment-variable names), providers such as [dsh-credentials-local](../../packages/credentials/credentials-local) own the values, and consumers resolve a reference once per operation — the LLM adapters resolve once per model request, so a rotated credential reaches the very next request without any restart. One seam-wide rule binds every provider: an empty stored value is absent everywhere. + +Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts) + +## Identity + +A reference names one credential as a POSIX-style environment-variable name. The brand keeps references from mixing with other cross-boundary strings; construction validates the shell-identifier shape. + +```ts type-equiv +/** Nominal reference to one credential: a POSIX-style environment-variable name. */ +type CredentialRef = Branded<'CredentialRef'> +``` + +## Resolution + +`resolve(ref)` returns the value with the provider-defined source layer that supplied it, or `undefined` while unconfigured. Consumers re-resolve at each operation and never cache across operations — that per-operation read is the hot-update mechanism. + +```ts type-equiv +/** One resolved credential value and the source layer that supplied it. */ +interface ResolvedCredential { + /** The non-empty secret value. */ + value: string + /** Provider-defined source layer id (the local provider uses `env`, `file`, `project-env`, and `user-env`). */ + source: string +} +``` + +## Description + +`describe(ref)` answers configuration surfaces without ever exposing a value: whether the reference resolves, from which layer, and whether `set` would currently succeed. The local provider reports a reference supplied by the live process environment as `writable: false` — a write would appear to succeed while resolution kept returning the shadowing value, so the seam rejects it and the UI can render the reference read-only up front. + +```ts type-equiv +/** Source and writability facts for one reference, safe for configuration UIs — never the value. */ +interface CredentialInfo { + /** Whether {@link Credentials.resolve} would currently return a value. */ + configured: boolean + /** Source layer currently supplying the value; absent while unconfigured. */ + source?: string + /** Whether {@link Credentials.set} would currently succeed for this reference. */ + writable: boolean +} +``` + +## 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 new file mode 100644 index 0000000000..6f5ccd1572 --- /dev/null +++ b/docs/subsystems/credentials.zh.md @@ -0,0 +1,133 @@ +# 用户凭据 + +[English](credentials.md) | 中文 + +[dsh-credentials](../../packages/credentials/credentials) 的凭据 seam 把机密挡在配置之外:settings 分节与 `cordis.yml` 条目携带的是*引用*(环境变量名),值归 [dsh-credentials-local](../../packages/credentials/credentials-local) 这类 provider 所有,消费方每个操作解析一次引用——LLM 适配器每次模型请求解析一次,因此轮换后的凭据无需任何重启即可作用于紧随其后的下一次请求。一条 seam 级规则约束每个 provider:空的存储值在任何地方都视为不存在。 + +Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts) + +## 标识 + +引用以 POSIX 风格环境变量名命名一条凭据。brand 使引用不与其他跨边界字符串混用;构造时校验 shell 标识符形态。 + +```ts type-equiv +/** Nominal reference to one credential: a POSIX-style environment-variable name. */ +type CredentialRef = Branded<'CredentialRef'> +``` + +## 解析 + +`resolve(ref)` 返回值,连同供出该值、由 provider 定义的来源层;未配置期间返回 `undefined`。消费方在每个操作中重新解析,绝不跨操作缓存——这次按操作进行的读取正是热更新机制。 + +```ts type-equiv +/** One resolved credential value and the source layer that supplied it. */ +interface ResolvedCredential { + /** The non-empty secret value. */ + value: string + /** Provider-defined source layer id (the local provider uses `env`, `file`, `project-env`, and `user-env`). */ + source: string +} +``` + +## 描述 + +`describe(ref)` 在绝不暴露值的前提下回应配置界面:引用当前是否可解析、来自哪一层、`set` 当前能否成功。本地 provider 把由活跃进程环境供值的引用报告为 `writable: false`——那样的写入会表面成功而解析持续返回遮蔽值,因此 seam 直接拒绝,界面也得以提前把该引用渲染为只读。 + +```ts type-equiv +/** Source and writability facts for one reference, safe for configuration UIs — never the value. */ +interface CredentialInfo { + /** Whether {@link Credentials.resolve} would currently return a value. */ + configured: boolean + /** Source layer currently supplying the value; absent while unconfigured. */ + source?: string + /** Whether {@link Credentials.set} would currently succeed for this reference. */ + writable: boolean +} +``` + +## 变更提交 + +`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 new file mode 100644 index 0000000000..e57b2aef45 --- /dev/null +++ b/docs/subsystems/filesystem.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/filesystem.md +filesystem.md: 591f1b4d4b1177ccbe7c2280d2054da5d4be8b99 +filesystem.zh.md: d599e48c1fba2d564a544f628ca3475b781e5cd5 diff --git a/docs/core-data-structures/filesystem.md b/docs/subsystems/filesystem.md similarity index 59% rename from docs/core-data-structures/filesystem.md rename to docs/subsystems/filesystem.md index addded9f67..591f1b4d4b 100644 --- a/docs/core-data-structures/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) @@ -256,6 +256,214 @@ type FsErrorCode = `FS_NOT_DIRECTORY`, `FS_PERMISSION_DENIED`, and `FS_IO_ERROR` are used by directory listing to distinguish an existing non-directory target, a denied listing, and an unexpected backend I/O failure. `FS_SANDBOX_DENIED` is a POLICY refusal from a sandbox-enforcing backend (`dsh-fs-sandbox`) — the mode fence denied a write/edit — distinct from `FS_PERMISSION_DENIED` (the host kernel refusing). `FS_NOT_OBSERVED` means the policy plugin has no prior-observation record for this owner (or a `createIfAbsent` hit an existing file). `FS_STALE_VERSION` means the backend version no longer matches the observed one (or an edit hit a missing target). Freshness authorization has no partial/full distinction, so there is no `FS_PARTIAL_OBSERVATION`. +## No timeouts on file IO + +`read`/`write`/`edit` take **no** `timeoutMs`, and the provider seam arms no deadline — unlike bash and web (which consume [`@deepseek-ai/dsh-timeout`](../../packages/util/timeout/README.md)) and the bash-backed `glob`/`grep` (whose declared `timeoutMs` is enforced by `@deepseek-ai/dsh-timeout-policy`): those are process-backed, where a deadline can really kill the work. A local syscall is best-effort-abortable at most — a timeout could not force an in-progress `fsync`/`rename` to stop, so a deadline here would be a knob that cannot deliver on its promise, and an implicit default in the exact place explicit-over-implicit forbids. Both reference agents (Claude Code, Codex) leave file IO untimed for the same reason; cancellation still propagates through the tool-execution signal for best-effort abort at syscall boundaries. + ## 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/core-data-structures/filesystem.zh.md b/docs/subsystems/filesystem.zh.md similarity index 59% rename from docs/core-data-structures/filesystem.zh.md rename to docs/subsystems/filesystem.zh.md index 1e378928ed..d599e48c1f 100644 --- a/docs/core-data-structures/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) 展示确切签名。 ## 执行上下文(策略插件) @@ -256,6 +256,214 @@ type FsErrorCode = 目录列表使用 `FS_NOT_DIRECTORY`、`FS_PERMISSION_DENIED` 与 `FS_IO_ERROR` 区分已存在但并非目录的目标、被拒绝的列表操作和意外的后端 I/O 失败。`FS_SANDBOX_DENIED` 是强制执行沙箱的后端(`dsh-fs-sandbox`)所作的策略拒绝——模式边界拒绝了写入/编辑——与 `FS_PERMISSION_DENIED`(宿主内核拒绝)不同。`FS_NOT_OBSERVED` 表示策略插件没有此所有者的先前观察记录(或 `createIfAbsent` 遇到了现有文件)。`FS_STALE_VERSION` 表示后端版本不再与观察到的版本匹配(或编辑操作遇到缺失目标)。新鲜度授权没有部分/完整之分,因此不存在 `FS_PARTIAL_OBSERVATION`。 +## 文件 IO 不设超时 + +`read`/`write`/`edit` **不**接受 `timeoutMs`,提供方 seam 也不设置截止时间——不同于 bash 与 web(它们消费 [`@deepseek-ai/dsh-timeout`](../../packages/util/timeout/README.md))以及 bash 支撑的 `glob`/`grep`(其声明的 `timeoutMs` 由 `@deepseek-ai/dsh-timeout-policy` 强制执行):那些是进程支撑的,截止时间可以真正终止工作。本地系统调用至多是尽力中止——超时无法迫使进行中的 `fsync`/`rename` 停下,因此这里的截止时间会成为无法兑现承诺的旋钮,而且恰好落在"显式优于隐式"禁止隐式默认值的位置。两个参照 agent(Claude Code、Codex)出于同样原因不给文件 IO 计时;取消仍通过工具执行 signal 传播,在系统调用边界尽力中止。 + ## 服务与插件 -`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 new file mode 100644 index 0000000000..945ed658d2 --- /dev/null +++ b/docs/subsystems/goal.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/goal.md +goal.md: 6f54a5261cb44c3fda389e37cb689a00061ab241 +goal.zh.md: 043263f3231b4c727f21ca0b8fc07d8adffa0559 diff --git a/docs/core-data-structures/goal.md b/docs/subsystems/goal.md similarity index 51% rename from docs/core-data-structures/goal.md rename to docs/subsystems/goal.md index fc6a7e63e5..6f54a5261c 100644 --- a/docs/core-data-structures/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/core-data-structures/goal.zh.md b/docs/subsystems/goal.zh.md similarity index 51% rename from docs/core-data-structures/goal.zh.md rename to docs/subsystems/goal.zh.md index c584cb375b..043263f323 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..572039f635 --- /dev/null +++ b/docs/subsystems/http-server.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/http-server.md +http-server.md: b9795fe98b432b6ef5f7d01a4d3e115c809fe642 +http-server.zh.md: b84ea457bcd37eed361fde88fbf680d189aeebfb diff --git a/docs/subsystems/http-server.md b/docs/subsystems/http-server.md new file mode 100644 index 0000000000..b9795fe98b --- /dev/null +++ b/docs/subsystems/http-server.md @@ -0,0 +1,108 @@ +# HTTP Server + +English | [中文](http-server.zh.md) + +[dsh-host-webserver](../../packages/host/webserver) is the web-shape HTTP carrier for the GUI host: a single `node:http` plugin providing `ctx.httpServer`, a named-route registry, index.html transform taps, and a single claimable fallback seat. It is not part of the agent-loop spine and not a capability seam — it knows no harness concepts, and every feature surface (the `/api` bridge, plugin bundles, the HMR event stream) is a route some other plugin registers ([layering note](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)). Web (browser) shape only: Electron loads dist over `file://` and carries fetch over an IPC bridge, not this server. + +Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts) + +## Routes + +```ts type-equiv +/** Route match kind: 'exact' matches the pathname verbatim; 'prefix' p matches p and p/. */ +type WebRouteKind = 'exact' | 'prefix' +``` + +```ts type-equiv +/** One named route registration. */ +interface WebRoute { + kind: WebRouteKind + /** Absolute pathname, no trailing slash. */ + path: string + /** Owns the full response lifecycle (may hold the response open, e.g. SSE). */ + handler: (req: IncomingMessage, res: ServerResponse) => void | Promise +} +``` + +Match order is fixed: exact table first, then longest matching prefix, then the registered fallback. Registration order carries no request-facing semantics — named routes are composed to be disjoint, and the fallback seat answers anything no named route claims; one owner only, a second registration throws. The shipped Web composition claims the seat with [`dsh-frontend-static`](../../packages/host/frontend-static/src/index.ts), the SPA dist server with locked semantics: non-GET/HEAD is 405, traversal outside the dist root is 403, any miss falls back to `index.html` with HTTP 200 (SPA routing), and unknown extensions ship as octet-stream. + +## Config + +```ts type-equiv +/** Gateway config: the listen address. */ +interface Config { + /** Listen host; the two supported values are loopback and all-interfaces. */ + host: '127.0.0.1' | '0.0.0.0' + /** Listen port; zero requests an OS-assigned port. */ + port: number +} +``` + +`host` accepts only `127.0.0.1` (default posture) and `0.0.0.0` (deliberate network exposure); there is no TLS, auth, or origin policy, so a non-loopback bind exposes the server to that network. The dist location is an assembly fact of the frontend plugin that claims the seat. + +## The service + +`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 new file mode 100644 index 0000000000..b84ea457bc --- /dev/null +++ b/docs/subsystems/http-server.zh.md @@ -0,0 +1,108 @@ +# HTTP 服务器 + +[English](http-server.md) | 中文 + +[dsh-host-webserver](../../packages/host/webserver) 是 GUI 宿主 web 形态的 HTTP 载体:单个提供 `ctx.httpServer` 的 `node:http` 插件,由具名路由注册表、index.html 转换挂点与单一可认领的回退席位组成。它不属于 agent loop(智能体循环)主干,也不是能力 seam:它不了解任何 harness 概念,每个功能表面(`/api` 桥接、插件 bundle、HMR(热模块替换)事件流)都是由其他插件注册的一条路由([分层说明](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md))。仅限 web(浏览器)形态:Electron 通过 `file://` 加载 dist,并经 IPC 桥接承载 fetch,不经过本服务器。 + +源码:[`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts) + +## 路由 + +```ts type-equiv +/** Route match kind: 'exact' matches the pathname verbatim; 'prefix' p matches p and p/. */ +type WebRouteKind = 'exact' | 'prefix' +``` + +```ts type-equiv +/** One named route registration. */ +interface WebRoute { + kind: WebRouteKind + /** Absolute pathname, no trailing slash. */ + path: string + /** Owns the full response lifecycle (may hold the response open, e.g. SSE). */ + handler: (req: IncomingMessage, res: ServerResponse) => void | Promise +} +``` + +匹配顺序固定:先查 exact 表,再取最长匹配前缀,最后落到已注册的回退。注册顺序不携带任何面向请求的语义:具名路由在组合上互不相交,任何未被具名路由认领的请求都由回退席位应答;席位只有一个所有者,第二次注册会抛出异常。发布的 Web 组合用 [`dsh-frontend-static`](../../packages/host/frontend-static/src/index.ts) 认领席位,即遵循固定语义的 SPA dist 服务器:非 GET/HEAD 返回 405,越出 dist 根目录的遍历返回 403,任何未命中都以 HTTP 200 回退到 `index.html`(SPA 路由),未知扩展名按 octet-stream 发送。 + +## 配置 + +```ts type-equiv +/** Gateway config: the listen address. */ +interface Config { + /** Listen host; the two supported values are loopback and all-interfaces. */ + host: '127.0.0.1' | '0.0.0.0' + /** Listen port; zero requests an OS-assigned port. */ + port: number +} +``` + +`host` 只接受 `127.0.0.1`(默认姿态)和 `0.0.0.0`(刻意的网络暴露);没有 TLS、认证或 origin 策略,因此绑定到非回环地址会把服务器暴露给该网络。dist 位置是认领席位的前端插件的组装事实。 + +## 服务 + +`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 new file mode 100644 index 0000000000..8ef657b3a4 --- /dev/null +++ b/docs/subsystems/invariants.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/invariants.md +invariants.md: 184d982bb27ab3ff98f715f0751c8bfcc1d08bea +invariants.zh.md: bd2f83a018642832fe50ba5cca6dd82e2bd7d59b diff --git a/docs/subsystems/invariants.md b/docs/subsystems/invariants.md new file mode 100644 index 0000000000..184d982bb2 --- /dev/null +++ b/docs/subsystems/invariants.md @@ -0,0 +1,88 @@ +# Runtime Invariants + +English | [中文](invariants.zh.md) + +[dsh-invariants](../../packages/support/invariants) is the configurable registry service (`ctx.invariants`) for package-owned runtime invariant checks. It is one support-group package, not a three-package capability seam, and not part of the agent-loop spine: the registry owns selection, name reservation, child-fiber lifecycle, and package-attributed failure, while every workspace package publishes a `./invariant` companion plugin that registers checks under its exact npm package name. What a check may assert — authoritative event streams or mutable data, never service or method presence — is the runtime-invariants convention in [AGENTS.md](../../AGENTS.md#conventions); the seam design is owned by the [invariant-service Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md). + +Source: [`packages/support/invariants/src/index.ts`](../../packages/support/invariants/src/index.ts) + +## Selection + +```ts type-equiv +/** Runtime invariant selection configured on the service plugin. */ +interface Config { + /** Global switch; defaults to `true`. */ + readonly enabled?: boolean + /** Case-sensitive JavaScript regex sources that admit package names; empty admits all. */ + readonly package_allowlist?: string[] + /** Case-sensitive JavaScript regex sources that exclude package names after allowlist matching. */ + readonly package_blocklist?: string[] +} +``` + +A package is selected when the service is enabled, the allowlist is empty or at least one pattern matches its full npm name, and no blocklist pattern matches — a blocklist match overrides an allowlist match. Entries compile with `new RegExp(source)`: matching is unanchored unless the source supplies `^` and `$`, and `/pattern/flags` syntax is not parsed. Validation fails loud at service startup: a blank, whitespace-padded, duplicate, or invalid entry throws instead of being skipped. A valid pattern may match no currently loaded package, so later loading and HMR stay deterministic; filters are fixed for the service lifetime ([README](../../packages/support/invariants/README.md)). + +## The installer + +```ts type-equiv +/** + * Throw a package-attributed invariant failure. + * @param message - violated package contract without the standard prefix. + * @returns never because reporting a violation throws. + */ +type InvariantFailure = (message: string) => never +``` + +```ts type-equiv +/** Install one package's checks into the registration's child context. */ +interface InvariantInstaller { + /** + * Install the package contribution. + * @param ctx - child context owned by this invariant registration. + * @param fail - reporter bound to the registering package name. + * @returns nothing, or a promise settling after asynchronous checks finish. + */ + (ctx: Context, fail: InvariantFailure): void | Promise + /** Services the child installer fiber may access. */ + readonly inject?: Inject +} +``` + +An enabled installer runs in a dedicated child Cordis fiber; `installer.inject` declares the services that fiber may access, and synchronous or asynchronous installer completion is joined before the registration succeeds. `fail(message)` throws `InvariantError` — `extends Error` with stable `code: 'INVARIANT'`, the owning `packageName`, and a message prefixed `invariant violated by "": …` — so a violation is attributable without the registry importing any product package. + +## The service + +`ctx.invariants.register(packageName, installer)` reserves one active registration for the full npm package name and returns its effect-scoped disposer. The reservation holds even when filters keep the installer inactive, so two plugins can never silently claim the same package name; a duplicate, blank, or whitespace-containing name throws. An installer failure disposes the child fiber and releases the reservation atomically. The service owns every registration fiber while the returned disposer also belongs to the companion fiber: unloading either side removes listeners, trace state, and the reservation, so a companion can reload and register the same name again without retained state. + +## 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 new file mode 100644 index 0000000000..bd2f83a018 --- /dev/null +++ b/docs/subsystems/invariants.zh.md @@ -0,0 +1,88 @@ +# 运行时不变式 + +[English](invariants.md) | 中文 + +[dsh-invariants](../../packages/support/invariants) 是面向包(package)自有运行时不变式检查的可配置注册表服务(`ctx.invariants`)。它是一个 support 组的包,不是三包能力 seam,也不属于 agent loop(智能体循环)主干:注册表拥有选择逻辑、名称保留、子 fiber 生命周期和归因到包的失败,而每个工作区包发布一个 `./invariant` 配套插件,以自己确切的 npm 包名注册检查。检查可以断言什么(权威事件流或可变数据,绝不是服务或方法是否存在)是 [AGENTS.md](../../AGENTS.md#conventions) 中的运行时不变式约定;seam 设计由[不变式服务 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md)规定。 + +源码:[`packages/support/invariants/src/index.ts`](../../packages/support/invariants/src/index.ts) + +## 选择 + +```ts type-equiv +/** Runtime invariant selection configured on the service plugin. */ +interface Config { + /** Global switch; defaults to `true`. */ + readonly enabled?: boolean + /** Case-sensitive JavaScript regex sources that admit package names; empty admits all. */ + readonly package_allowlist?: string[] + /** Case-sensitive JavaScript regex sources that exclude package names after allowlist matching. */ + readonly package_blocklist?: string[] +} +``` + +一个包被选中的条件是:服务已启用,允许列表为空或至少一个模式匹配其完整 npm 名称,且没有任何阻止列表模式匹配;阻止列表匹配优先于允许列表匹配。条目用 `new RegExp(source)` 编译:除非模式自带 `^` 和 `$`,匹配不锚定;`/pattern/flags` 语法不被解析。校验在服务启动时大声失败:空白、首尾带空白、重复或无效的条目会抛出异常,而不是被跳过。有效模式可以不匹配任何当前已加载的包,因此后续加载与 HMR(热模块替换)保持确定性;过滤器在服务生命周期内固定不变([README](../../packages/support/invariants/README.md))。 + +## 安装器 + +```ts type-equiv +/** + * Throw a package-attributed invariant failure. + * @param message - violated package contract without the standard prefix. + * @returns never because reporting a violation throws. + */ +type InvariantFailure = (message: string) => never +``` + +```ts type-equiv +/** Install one package's checks into the registration's child context. */ +interface InvariantInstaller { + /** + * Install the package contribution. + * @param ctx - child context owned by this invariant registration. + * @param fail - reporter bound to the registering package name. + * @returns nothing, or a promise settling after asynchronous checks finish. + */ + (ctx: Context, fail: InvariantFailure): void | Promise + /** Services the child installer fiber may access. */ + readonly inject?: Inject +} +``` + +被启用的安装器在专属的子 Cordis fiber 中运行;`installer.inject` 声明该 fiber 可以访问的服务,注册成功之前会先等待安装器同步或异步地执行完毕。`fail(message)` 抛出 `InvariantError`(`extends Error`,带稳定的 `code: 'INVARIANT'`、所属 `packageName`,以及前缀为 `invariant violated by "": …` 的消息),因此违规可归因,而注册表无需导入任何产品包。 + +## 服务 + +`ctx.invariants.register(packageName, installer)` 为完整 npm 包名保留唯一一个活跃注册,并返回其绑定到 effect 的 disposer。即使过滤器使安装器保持不活跃,保留依然成立,因此两个插件绝不可能静默地认领同一个包名;重复、空白或含空白字符的名称会抛出异常。安装器失败会原子地 dispose(资源释放)子 fiber 并释放保留。服务拥有每个注册 fiber,而返回的 disposer 同时属于配套插件的 fiber:卸载任一侧都会移除监听器、trace 状态和保留项,因此配套插件可以重载并再次注册同一名称,不留残余状态。 + +## 配套插件契约 + +每个工作区包都拥有一个 `./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/core-data-structures/approval.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml similarity index 54% rename from docs/core-data-structures/approval.i18n.yaml rename to docs/subsystems/llm-streaming.i18n.yaml index 0679cd47bd..f807656000 100644 --- a/docs/core-data-structures/approval.i18n.yaml +++ b/docs/subsystems/llm-streaming.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/approval.md -approval.md: f1889b25e2bbbcb157b0bced070b1f157a867504 -approval.zh.md: 48222991312f9ae97c9249f1232b261d2393d282 +# pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md +llm-streaming.md: c65416f449d1783f398cdd81b0d6987de83a6141 +llm-streaming.zh.md: ce79ecca53edf7213853df118489817451baa7b0 diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md new file mode 100644 index 0000000000..c65416f449 --- /dev/null +++ b/docs/subsystems/llm-streaming.md @@ -0,0 +1,885 @@ +# LLM Streaming + +English | [中文](llm-streaming.zh.md) + +The conversation and streaming vocabulary of [`packages/llm`](../../packages/llm/README.md): the `Message`/`ContentBlock` shapes every request and durable history share, the fully-assembled model request, the raw `StreamChunk` protocol, the adapter contract every adapter must obey, and the shared assembler. The [core spine](core.md) holds and logs these values on every turn; this page declares them. + +Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + + + +## Content blocks and messages + +A conversation is `Message`s; a message is an array of typed **content blocks**. The block union derives from `ContentBlockMap`. + +Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + +```ts type-equiv +/** + * Merge-extensible content blocks keyed by `type`. New core blocks must land + * with adapter, UI, and compaction support. + */ +interface ContentBlockMap { + 'text': TextBlock + 'reasoning': ReasoningBlock + 'tool-call': ToolCallBlock + 'tool-result': ToolResultBlock +} +``` + +The block interfaces (full fields in source): `TextBlock` (`text`), `ReasoningBlock` (thinking, distinct from visible text), `ToolCallBlock` (`id: CallId`, `name`, raw-JSON `arguments`), `ToolResultBlock` (`toolCallId`, nested `content: ContentBlock[]`, `isError?`). `ContentBlock = ContentBlockMap[ContentBlockType]`. The core set is limited to blocks every shipping path honors — multimodal content (images, audio, …) has no core block type; a feature that needs one adds it via the merge-extensible map together with the adapter/UI/compaction support that honors it. + +Source: [`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts) + +A `Message` is one identified, immutable role/source/content value. Model-produced assistant messages carry provider/model ownership and optional adapter-private replay metadata in their source: + +```ts type-equiv +/** Provider ownership and adapter-private replay data for an assistant message. */ +interface AssistantProvenance { + /** Provider route that produced the message. */ + provider: string + /** Provider model id that produced the message. */ + model: string + /** + * Lossless-JSON adapter state needed to replay the provider response. + * `LlmService` exposes it to a target adapter only when that adapter instance + * currently owns both this historical provider and the target provider. + */ + replayState?: unknown +} +``` + +```ts type-equiv +/** One immutable message representation shared by delivery, durable history, and model requests. */ +interface Message { + /** Stable identity preserved across every representation boundary. */ + readonly id: MessageId + /** Provider-neutral conversation role. */ + readonly role: 'system' | 'user' | 'assistant' + /** Exact model-facing blocks. */ + readonly content: ContentBlock[] + /** Required producer provenance. */ + readonly source: MessageSource +} +``` + +Where a message came from is itself a merge-extensible sum type: + +```ts type-equiv +/** + * Where a message (or injected content) came from. + * Merge-extensible sum type — plugins add their own `kind`s. + */ +interface MessageSourceMap { + user: { kind: 'user' } + plugin: { kind: 'plugin'; plugin: string } & ContextFormed + model: ModelMessageSource + tool: ToolMessageSource +} +``` + +Provenance and shape are two independent axes. `kind` answers *who produced this*; the optional `form` a producer mixes in answers *what shape of information it is*, so several producers may share one presentation and one producer may emit more than one shape over a session. The vocabulary is semantic and grows one value at a time; an absent or unrecognized value is the documented default, presented as opaque content: + +```ts type-equiv +/** + * What SHAPE of information a producer-supplied context carries, declared by + * the producer beside its provenance. + * + * `MessageSource.kind` answers *who produced this*; `form` answers *what kind + * of thing it is*, and the two axes are deliberately independent — several + * producers share one form (three snapshot producers today), and one producer + * may emit more than one form over a session. + * + * The vocabulary is SEMANTIC, never visual: a value states that the content is + * a file's instructions or a catalog of available items, and a consumer decides + * what that looks like. Colors, icons, ordering, and collapse defaults are the + * consumer's business and must not enter this union. It grows one value at a + * time as producers gain the structured fields their form needs; an absent or + * unknown value is the documented default, presented as opaque content. + */ +type ContextForm = + /** Instructions read out of workspace files the model is expected to follow. */ + | 'instructions' + /** A catalog of items available in this session, republished as it changes. */ + | 'catalog' + /** Current state, where a later snapshot from the same producer supersedes an earlier one. */ + | 'snapshot' + /** A one-off account of something that just happened; it supersedes nothing. */ + | 'notice' + /** A message another agent addressed to this one. */ + | 'relay' + /** Material lifted out of another session's log, possibly reduced on the way in. */ + | 'recall' +``` + +```ts type-equiv +/** One named contribution to a `snapshot`-form context, in assembly order. */ +interface ContextSnapshotSection { + /** The contributing subsystem's name. */ + readonly name: string + /** That contribution's model-facing text, exactly as assembled. */ + readonly text: string +} +``` + +```ts type-equiv +/** + * Producer-declared {@link ContextForm} and the fields that form requires, + * mixed into the source shapes that carry one. + * + * Discriminated by `form` so a producer cannot declare a shape without the + * facts that shape is presented from: a `notice` must record its one-line + * account, a `snapshot` its sections. Omitting `form` stays valid — an + * undeclared context is the documented default. + */ +type ContextFormed = + | { readonly form?: never } + | { readonly form: 'instructions' } + | { readonly form: 'catalog' } + | { + readonly form: 'snapshot' + /** The named contributions this snapshot assembled, in order. */ + readonly sections: readonly ContextSnapshotSection[] + } + | { + readonly form: 'notice' + /** One-line account of what happened, shown without expanding the row. */ + readonly summary: string + } + | { readonly form: 'relay' } + | { readonly form: 'recall' } +``` + +## `StreamChunk` — the raw protocol + +A streaming response interleaves several typed blocks (text, reasoning, multiple tool calls). `index` ties each delta to its block; `block-end` carries the fully-assembled `ContentBlock` so consumers don't have to re-assemble deltas themselves. It is a **closed** discriminated union — a `switch` over `type` ends with `assertNever`, so adding a variant breaks compilation at every consumer that must handle it. + +```ts type-equiv +/** + * Raw streaming protocol emitted by adapters. + * Block indexes correlate interleaved deltas, and `block-end` carries the + * assembled block. Adapters emit usage before the terminal finish and nothing + * afterward; tool arguments remain raw JSON strings. An adapter implementation + * may throw, but `LlmService.stream()` normalizes that failure to a terminal + * `error` or `aborted` finish before exposing it to consumers. + */ +type StreamChunk = + | { type: 'block-start'; index: number; blockType: ContentBlockType } + | { type: 'text-delta'; index: number; text: string } + | { type: 'reasoning-delta'; index: number; text: string } + | { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string } + | { type: 'block-end'; index: number; block: ContentBlock } + | { type: 'usage'; usage: TokenUsage } + | { + type: 'finish' + reason: FinishReason + /** Adapter-private lossless-JSON state for replaying a successful response. */ + replayState?: unknown + } +``` + +## `LlmFailure` + +Every thrown or in-band final-adapter failure normalizes to one serializable provider-neutral payload. `providerRetryAfterMs` is a validated positive delay requested by the provider, not a retry decision; `ProviderRequestId` is an opaque branded string for diagnostics. + +```ts type-equiv +/** Serializable provider-boundary facts; policy decides whether they are retryable. */ +interface LlmFailure { + /** Human-readable provider or transport failure. */ + readonly message: string + /** Stable provider-neutral machine-routing code. */ + readonly code: string + /** HTTP status observed at the provider boundary, when available. */ + readonly status?: number + /** Provider-requested delay in milliseconds, when valid and available. */ + readonly providerRetryAfterMs?: number + /** Opaque provider-issued request identifier for diagnostics. */ + readonly requestId?: ProviderRequestId +} +``` + +## The adapter contract + +Every adapter MUST obey these, and every consumer may rely on them: + +- **`usage` before `finish`, nothing after `finish`.** Defer both to the provider's end-of-stream marker so a trailing usage-only chunk can't violate the ordering. +- **Tool-call `arguments` stay raw JSON strings end-to-end.** Partial fragments stream via `argumentsDelta`; a provider that hands back parsed objects re-stringifies at `block-end`. +- **Two sanctioned error paths, one fact shape.** A failure may either THROW from `stream()` (transport/protocol errors) **or** end the stream with `finish {kind:'error'|'aborted', failure}` (provider in-band errors, for adapters that can't throw mid-stream). `LlmError.failure` carries the same `LlmFailure`. The final adapter boundary preserves the exact thrown `Error` object and associates immutable facts plus the serving registration's immutable retry policy with that call; the agent loop closes the failed step and offers the error, facts, immutable prior-retried facts, serving policy, and turn signal to `agent/request-error`. A handling listener returns `{ kind: 'retry' }` after its awaited repair; absent recovery the structured failure becomes the turn error, and no normal assistant message or tool side effect is committed for that attempt. +- **One adapter call is one provider attempt.** Adapters disable library retries. Agent-level recovery opens another durable numbered turn; direct `ctx.llm.stream()` callers remain single-attempt. +- **Provider stalls are bounded at the transport.** Both shipping remote adapters expose positive finite `streamIdleTimeoutMs` with a five-minute default. The watchdog arms only while iterator `next()` is outstanding, uses one stable signal for the whole request, maps its own expiry to `TIMEOUT`, and keeps an earlier caller abort as `ABORTED`. +- **Context overflow has one canonical code.** Both DeepSeek adapters classify explicit provider detail through `isContextWindowExceededError()` and surface `CONTEXT_WINDOW_EXCEEDED`, whether the failure arrives as a thrown HTTP `LlmError` or an in-band finish error. Consumers route on the code, never provider text. +- **An empty completion is a retryable error, not a silent success.** Both adapters map a terminal `stop` finish that carried no content blocks to `finish {kind:'error'}` with the canonical `EMPTY_RESPONSE` code, and `dsh-llm-retry` retries it by default; see [empty model responses are retryable](../../.agents/notes/implemented/bug-fix/2026-07-24-empty-model-response-is-retryable.md). +- **Every provider HTTP request carries the app-attribution header.** Adapters send `attributionHeaders()` (below) - the `User-Agent` baseline - and prove it with a wire-level test (mock server asserting the received header, or the library's header hook for a library-backed adapter). +- **Replay state is adapter-owned.** A successful `finish` may carry lossless-JSON state needed to reconstruct a native provider response. The loop stores it with the assembled assistant message. On a later request, `LlmService` passes the state only when the historical provider and target provider are currently registered to the exact same adapter instance. That adapter validates the state and owns any cross-model or cross-provider conversion; other adapters receive the provider-neutral content and provenance without the private state. + +This contract is pinned down by two deliberately independent implementations: `dsh-llm-deepseek` (direct fetch, SSE framing via `eventsource-parser`) and `dsh-llm-pi-ai` (a generic multi-provider adapter through `@earendil-works/pi-ai`). The library-backed adapter exercises the finish-chunk error path, while transport-boundary tests prove each idle watchdog stops its actual request. + +## `ResolvedRetryPolicy` + +Provider configuration resolves before route registration into an immutable discriminated union. Normal mode carries `mode: 'normal'`, finite `maxRetries`, `retryableCodes`, and required `initialDelayMs`, `maxDelayMs`, and `jitterRatio`; always mode carries `mode: 'always'` and the same required backoff fields without a finite maximum. `LlmService.providerRetryPolicy(provider)` returns the currently registered value and supplies normal defaults when the adapter omits one; `llmRetryPolicyOf(stream)` returns the exact serving registration's captured value after that call enters its final adapter boundary, so later route disposal or replacement cannot change an in-flight failure's recovery policy. The [generated config catalog](../config-catalog.md) owns the optional input shapes. + +## `AppIdentity` — app attribution + +The static public application identity every adapter sends to providers ([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts)). `attributionHeaders(identity?)` maps it to the standard `User-Agent` header only; OpenRouter-specific app attribution headers are intentionally not supported by this contract. The default `APP_IDENTITY` sources its version from the package manifest; every field is a public product fact - no secrets, paths, session ids, or per-user identifiers, and nothing per-request may influence the values. Rationale: [Mandatory `User-Agent` attribution](../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md). + +```ts type-equiv +/** + * Static public application identity sent to LLM providers. + * + * Every field is a public product fact, safe on every request: no secrets, + * local paths, session ids, prompt text, or per-user identifiers belong here, + * and nothing per-request may influence the values. + */ +interface AppIdentity { + /** `User-Agent` product token (lowercase, hyphenated). */ + product: string + /** Product version; sourced from package metadata, never hand-copied. */ + version: string + /** Public home URL of the app, used as the `User-Agent` comment. */ + url: string +} +``` + +## `TokenUsage` + +Per-call token accounting. Counts are **disjoint**: `inputTokens` is uncached input only; cached input is reported separately, and billed input is the sum of the three. Adapters whose providers fold cache hits into a single prompt total (DeepSeek's `prompt_tokens`) subtract them back out. `reasoningTokens`, when present, is informational detail already included in `outputTokens`; totals must not add it again. + +```ts type-equiv +/** + * Token accounting for one model call (cache fields are optional). + * + * Counts are DISJOINT: `inputTokens` is uncached input only; cached input is + * reported separately as `cacheReadTokens`/`cacheWriteTokens` (billed input = + * sum of the three). Adapters whose providers fold cache hits into a total + * prompt count (DeepSeek's `prompt_tokens`) subtract them out. + */ +interface TokenUsage { + inputTokens: number + outputTokens: number + cacheReadTokens?: number + cacheWriteTokens?: number + reasoningTokens?: number +} +``` + +## `BlockAssembler` + +`BlockAssembler` ([`packages/llm/llm/src/assembler.ts`](../../packages/llm/llm/src/assembler.ts)) is the single shared implementation that folds a `StreamChunk` stream back into `ContentBlock`s, usage, finish reason, and replay state. The loop logs the raw chunks while feeding the same chunks through an assembler, then stores the assembled assistant content with its provider/model provenance. A consumer that needs the assembled result without re-implementing the fold uses this. + +```ts public-api +/** + * Incrementally assembles raw {@link StreamChunk}s into complete + * {@link ContentBlock}s and a final assistant {@link Message}. + * + * The agent loop feeds it while logging raw chunks for replay fidelity, then + * reads `blocks()` / `message()` / `usage` / `finish` once the stream ends. + * + * Tolerant of delta-only protocols (no block-start/end); deltas arriving for + * an index already closed by `block-end` are ignored (malformed stream) so a + * misbehaving adapter cannot grow memory or corrupt a completed block. + */ +declare class BlockAssembler { + /** + * Feed one chunk into the assembly state. + * @param chunk - the next raw chunk, in stream order. + */ + push(chunk: StreamChunk): void; + /** + * Assemble all blocks seen so far, in stream order. + * @returns one block per seen index, except that max-token truncation drops + * tool calls that cannot be executed safely; an open block assembles from + * its accumulated deltas (an unknown block type never closed by `block-end` throws). + */ + blocks(): ContentBlock[]; + /** Usage from the `usage` chunk; undefined until one arrives. */ + get usage(): TokenUsage | undefined; + /** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */ + get finish(): FinishReason; + /** Adapter-private replay state from the terminal finish chunk, if any. */ + get replayState(): unknown; + /** + * The assembled assistant message. + * @param source - producer attribution for the assembled message. + * @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules). + */ + message(source: MessageSource = { kind: 'plugin', plugin: 'dsh-llm/assembler' }): Message; +} +``` + + + +## The model request + +One model call is a fully-assembled `GenerateOptions`. The adapter answers with a raw [`StreamChunk`](#streamchunk--the-raw-protocol) stream; the consumer assembles it with [`BlockAssembler`](#blockassembler). + +Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + +Provider and model discovery uses small provider-neutral descriptors. A model catalog is advisory: routing still keys on a registered provider, and an adapter may accept unlisted model ids. + +Registering an adapter returns a handle: the disposer, plus the atomic route replacement a plugin whose route set is user-configurable needs. + +```ts type-equiv +/** + * What {@link LlmService.registerAdapter} returns: the disposer, plus an + * atomic route replacement for the same adapter instance. + */ +interface AdapterRegistrationHandle { + /** Release every route this registration currently holds. */ + (): void + /** + * Replace this registration's routes with `providers`, keeping the same + * adapter instance. The candidate set is validated in full first — a + * conflict with another adapter, an invalid name, or bad provider metadata + * throws and leaves the current routes untouched — and the swap itself is + * one synchronous section, so no request can observe a gap. An empty array + * is legal here (a settings section that emptied holds zero routes while + * staying registered), unlike an empty initial registration. + * + * Throws `LlmError` with code `REGISTRATION_DISPOSED` once the registration + * has been released: its routes are gone and its disposer has already run, + * so anything registered afterwards would have no owner left to release it. + * @param providers - the complete next route set for this registration. + */ + replace(providers: string[]): void +} +``` + +```ts type-equiv +/** Display metadata for one registered provider route. */ +interface LlmProviderInfo { + /** Provider route key used by {@link GenerateOptions.provider}. */ + id: string + /** Human-readable provider name for selectors and diagnostics. */ + name: string +} +``` + +Adapter plugins additionally declare which routes *could* run through `registerConfigurableProviders()`, addressing each one's user-settings section, so configuration surfaces can offer dormant providers before any route registers. + +```ts type-equiv +/** + * One provider route an adapter plugin can activate through configuration, + * whether or not the route is currently registered. Configuration surfaces + * merge this directory with `listProviders()` to offer every configurable + * provider alongside its live/dormant state. + */ +interface LlmConfigurableProvider { + /** Provider route key this entry activates when configured. */ + provider: string + /** Human-readable provider name for configuration surfaces. */ + displayName: string + /** User-settings namespace whose section configures this provider. */ + settingsNs: string + /** + * Path from that namespace's section root to this provider's profile + * object; empty when the whole section is the profile. + */ + settingsPath: readonly string[] + /** + * Whether the owning adapter knows this route only because configuration + * declared it — a gateway or self-hosted server it ships nothing about. + * Absent means the adapter draws no such distinction; false means it does + * and this route is one of its own. Only the adapter can answer: a stored + * profile is how a user-added route AND a corrected shipped one both look + * from outside. + */ + declared?: boolean +} +``` + +```ts type-equiv +/** One adapter-discovered model; catalog membership is advisory, not request validation. */ +interface LlmModelInfo { + /** Provider route that owns this model entry. */ + provider: string + /** Model id passed to {@link GenerateOptions.model}. */ + id: string + /** Human-readable model name for selectors. */ + name: string + /** Optional user-facing distinction from otherwise similar models. */ + description?: string +} +``` + +Correctness-sensitive metadata is resolved separately from the advisory catalog and is owned by the adapter serving the exact route. Context capacity, adapter call defaults, and reasoning choices share one exact-model result so consumers do not repeat authoritative model resolution. + +```ts type-equiv +/** Provider-owned context capacity for one exact provider/model route. */ +interface LlmModelContext { + /** Maximum combined request and response context in tokens. */ + contextWindow: number +} +``` + +Reasoning effort is another exact-route capability. The core brands identifiers but does not enumerate their values; each adapter owns the ordered set, display names, and optional deployment default. + +```ts type-equiv +/** Adapter-owned identifier for one model's selectable reasoning effort. */ +type ReasoningEffortId = Branded<'ReasoningEffortId'> +``` + +```ts type-equiv +/** Display metadata for one adapter-owned reasoning effort. */ +interface LlmReasoningEffortInfo { + /** Opaque stable value accepted by {@link GenerateOptions.reasoningEffort}. */ + id: ReasoningEffortId + /** Human-readable effort name for selectors and diagnostics. */ + name: string + /** Optional user-facing distinction from otherwise similar efforts. */ + description?: string +} +``` + +```ts type-equiv +/** Selectable reasoning efforts for one exact provider/model route. */ +interface LlmModelReasoningInfo { + /** Supported efforts in adapter-preferred display order. */ + efforts: readonly LlmReasoningEffortInfo[] + /** + * Adapter-configured default materialized into requests when callers omit + * an effort. Absence preserves the provider's own default. + */ + defaultEffort?: ReasoningEffortId +} +``` + +```ts type-equiv +/** Exact-route model metadata resolved by its owning adapter. */ +interface LlmResolvedModelInfo extends LlmModelInfo { + /** Provider-owned context capacity when known. */ + context?: LlmModelContext + /** Adapter-configured per-request output cap materialized when callers omit one. */ + defaultMaxTokens?: number + /** Adapter-owned selectable reasoning levels when exposed. */ + reasoning?: LlmModelReasoningInfo +} +``` + +```ts type-equiv +/** A single model request, fully assembled. */ +interface GenerateOptions { + /** Registered provider route selecting the adapter instance. */ + provider: string + model: string + /** Adapter-owned reasoning effort selected for this exact model. */ + reasoningEffort?: ReasoningEffortId + /** + * Ordered conversation messages, exactly as the provider sees them (after + * the `system` slot). A loop-built request assembles them as + * the derived history (dsh-agent-loop); a hand-built one-shot passes any list. + */ + messages: Message[] + /** System prompt text (adapters map to the provider's system slot). */ + system?: string + /** Tool schemas (adapters map to the provider's `tools` field). */ + tools?: ToolSchema[] + temperature?: number + maxTokens?: number + /** + * Stop sequences: generation halts as soon as the model produces any one of + * these strings (adapters map to the provider's stop field, e.g. OpenAI + * `stop`). The stop string itself is not included in the output. + */ + stop?: string[] + signal?: AbortSignal + /** + * Session identity stamped by the loop for listener routing. Adapters ignore + * it; replay uses it to keep concurrent parent and child cursors independent. + */ + sessionId?: Branded<'SessionId'> + /** + * Provider-neutral classification for an auxiliary model call. Adapters may + * map the purpose to model-hidden transport metadata or purpose-specific + * generation policy. Ordinary conversation requests leave it unset. + */ + purpose?: 'compaction' | 'session-title' +} +``` + +Why a model response stopped is a merge-extensible reason. Terminal provider failures carry the streaming contract's [`LlmFailure`](#llmfailure): + +```ts type-equiv +/** + * Why a model response stopped. + * Merge-extensible so adapters can surface provider-specific reasons. + */ +interface FinishReasonMap { + 'stop': { kind: 'stop' } + 'tool-calls': { kind: 'tool-calls' } + 'max-tokens': { kind: 'max-tokens' } + 'aborted': { kind: 'aborted'; failure: LlmFailure } + 'error': { kind: 'error'; failure: LlmFailure } +} +``` + +`FinishReason = FinishReasonMap[keyof FinishReasonMap]`. `TokenUsage` (per-call accounting with disjoint cache fields) is detailed [below](#tokenusage). + +`GenerateOptions.tools` carries `ToolSchema` — the JSON-schema description of a tool, as sent to the model. It is declared in dsh-llm (not dsh-tools) precisely because it is part of the request the loop assembles every step: + +```ts type-equiv +/** + * JSON-schema description of a tool, as sent to the model. + * + * Declared here (not in dsh-tools) because it is part of {@link GenerateOptions}; + * dsh-tools' ToolDefinition and dsh-system-prompt's PromptAssembly both import + * it from this package. + */ +interface ToolSchema { + name: string + description: string + /** JSON Schema object for the arguments. */ + parameters: Record +} +``` + +The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition` that produces it (schema + `execute`) is on [tools.md](tools.md). + +A provider a surface is still drafting has no route and no catalog, so interrogation is described separately: the request carries the draft the user is editing, and the reply is candidates a surface may adopt rather than a catalog it must serve. + +```ts type-equiv +/** + * One interrogation of a provider endpoint that configuration has not stored + * yet. Configuration surfaces send the draft a user is still editing, so the + * request carries the endpoint and credential directly instead of naming a + * route: a provider being added has no route to name. + */ +interface LlmModelDiscoveryRequest { + /** + * Route the draft is editing, when it edits an existing one. A route whose + * adapter already knows its models answers from that knowledge instead of + * asking the endpoint — the adapter's own registry is the better answer, and + * it costs no network call. + */ + provider?: string + /** + * Endpoint to interrogate. Optional because a route the adapter already + * describes needs none; a route it does not must supply one. + */ + baseURL?: string + /** Wire protocol the endpoint speaks, when the draft names one. */ + api?: string + /** Credential for this interrogation alone; the harness never stores it. */ + apiKey?: string + /** Caller cancellation; implementations must settle promptly after it aborts. */ + signal?: AbortSignal +} +``` + +```ts type-equiv +/** + * One model an endpoint reports about itself. Every field but the id is + * optional because most provider listings disclose an id and nothing else; + * a surface adopting one of these still owes the capacities its adapter needs. + */ +interface LlmDiscoveredModel { + /** Model id the endpoint accepts. */ + id: string + /** Human-readable name when the endpoint supplies one. */ + name?: string + /** Maximum combined request and response context, when disclosed. */ + contextWindow?: number + /** Maximum output tokens, when disclosed. */ + maxTokens?: number +} +``` + +### The request envelope: `LlmCallConfig` and the logged header + +The loop builds each request from logged state. `EpochHeader` records call config, adapter-default provenance, rendered prompt, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md). + +`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus provenance under the turn signal. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests. + +On the wire, a loop-built request reads the `system` slot (the rendered prompt assembly) followed by the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The dev invariant recomputes exactly this equation against every loop-built request. + +FIXME(call-config-shape): revisit which remaining fields are genuinely epoch-level for cache purposes (`model` and the model-owned reasoning effort are explicit; the sampling scalars sit here out of caution). + +```ts type-equiv +/** + * Provider, model, reasoning effort, and sampling scalars of one conversation's + * requests. Every field maps 1:1 onto the same-named `GenerateOptions` field; + * the loop builds requests from the logged header rather than accepting these + * per call. + */ +interface LlmCallConfig { + provider: string + model: string + reasoningEffort?: ReasoningEffortId + temperature?: number + maxTokens?: number + stop?: string[] +} +``` + +```ts type-equiv +/** + * Effective config fields supplied by exact-model adapter resolution rather + * than by the caller's request proposal. + */ +interface LlmCallConfigAdapterDefaults { + reasoningEffort?: true + maxTokens?: true +} +``` + +## The seam + +`LlmAdapter` is the provider seam: subclass, implement `stream()`, and register one adapter instance with `ctx.llm.registerAdapter(providers, adapter)`. `GenerateOptions.provider` selects the registered adapter; `GenerateOptions.model` is passed to that adapter and need not be registered at lifecycle start. Duplicate provider routes fail atomically. Optional `providerRetryPolicy()` is captured per route with normal defaults, while `providerInfo()` and asynchronous `listModels()` feed `LlmService.listProviders()` / `listModels()` with detached selector metadata. That catalog is advisory rather than a request whitelist: the adapter remains authoritative and may accept unlisted model ids. One asynchronous `resolveModel()` query returns exact model identity plus optional correctness-sensitive context capacity, an adapter-configured `defaultMaxTokens`, and ordered model-owned reasoning ids with an optional deployment default; absent fields mean unavailable metadata or provider-owned behavior, not invalid catalog membership. The resolver receives optional cancellation and must settle promptly after abort. `LlmService.resolveModelInfo()` validates and detaches the aggregate. At the final adapter boundary, `resolveCallConfig()` materializes the output default only when `maxTokens` is absent and validates and materializes reasoning, so direct calls cannot bypass either configured behavior; direct dispatch captures one registration before awaiting that resolution. The agent loop instead uses `prepareCall()` to keep the same registration across model resolution, durable header logging, and dispatch, retain detached context metadata from that exact lookup, and report which config fields the adapter defaulted. Adapter lookup happens at the terminal continuation of the `llm/stream` waterfall, so a listener may short-circuit the call or route a mutable one-shot request before lookup. AgentLoop observes a request attempt once the outer waterfall returns a stream handle; that limited boundary does not prove a lazy terminal adapter was constructed or began provider I/O. The `block-start` / `block-end` `index` correlation and the assembler together mean an adapter only has to emit well-formed chunks — block reassembly is not each adapter's problem. The consumer surface (`ctx.llm.stream()`) and the `llm/stream` waterfall are described in [architecture.md § Content blocks and streaming](../architecture.md#content-blocks-and-streaming-dsh-llm). + +```ts type-equiv +/** One model call whose config and adapter registration were resolved together. */ +interface PreparedLlmCall { + /** Detached, deep-frozen config with any adapter-owned default materialized. */ + readonly config: LlmCallConfig + /** Immutable retry policy captured with the adapter registration. */ + readonly retryPolicy: ResolvedRetryPolicy + /** Detached context metadata resolved with the registration-bound call. */ + readonly context?: LlmModelContext + /** Config fields materialized by the captured adapter rather than proposed by the caller. */ + readonly adapterDefaults: LlmCallConfigAdapterDefaults + /** + * Dispatch this call once through the registration captured during + * preparation. The request's call-config fields must match {@link config}; + * reuse or mismatch fails with `INVALID_PREPARED_CALL`. + * @param options - fully assembled request carrying the prepared config. + * @returns the chunk stream, including the `llm/stream` waterfall. + */ + stream(options: GenerateOptions): AsyncIterable +} +``` + +```ts public-api +/** + * Provider-wire adapter for the harness message and stream vocabulary. Register implementations + * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include + * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The direct-fetch + * DeepSeek and library-backed pi-ai adapters intentionally exercise this contract through different internals. + */ +declare abstract class LlmAdapter { + /** + * Describe one provider route owned by this adapter. + * @param provider - a route passed to `registerAdapter()` for this instance. + * @returns detached display metadata whose id must equal `provider`. + */ + providerInfo(provider: string): LlmProviderInfo; + /** + * Return the provider-owned retry policy captured with this route. + * @param _provider - a route passed to `registerAdapter()` for this instance. + * @returns a resolved policy, or `undefined` to use the normal defaults. + */ + providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined; + /** + * List models this adapter can currently advertise for one owned provider. + * The result is advisory: an adapter may accept unlisted model ids, and + * consumers must not turn absence into request rejection. + * @param _provider - one provider route owned by this adapter. + * @returns discoverable models in adapter-preferred order. + */ + listModels(_provider: string): Promise; + /** + * Resolve all metadata available for one exact model. This query is + * independent of the advisory catalog and does not validate request routing. + * @param provider - one provider route owned by this adapter. + * @param model - exact model id passed to {@link GenerateOptions.model}. + * @param _signal - cancellation for this exact-model lookup; asynchronous + * implementations must settle promptly after it aborts. + * @returns provider/model identity plus any context, call-default, and reasoning metadata. + */ + resolveModel( + provider: string, + model: string, + _signal?: AbortSignal, + ): Promise; + /** + * Stream one model call as raw chunks. The only required method. + * @param options - the fully-assembled request; implementations must honor `options.signal`. + * @returns the chunk stream, obeying the adapter contract documented on `StreamChunk`. + */ + abstract stream(options: GenerateOptions): AsyncIterable; +} +``` + +`ContentBlockType` (the key set the `index`-correlated blocks carry) derives from [`ContentBlockMap`](#content-blocks-and-messages) above. + + + + + +## 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 +``` + +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 +``` + +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 new file mode 100644 index 0000000000..ce79ecca53 --- /dev/null +++ b/docs/subsystems/llm-streaming.zh.md @@ -0,0 +1,893 @@ +# LLM(大语言模型)流式输出 + +[English](llm-streaming.md) | 中文 + +[`packages/llm`](../../packages/llm/README.md) 的对话与流式输出词汇:每个请求与持久历史共享的 `Message`/`ContentBlock` 形状、完整组装的模型请求、原始 `StreamChunk` 协议、每个适配器必须遵守的适配器契约(adapter contract),以及共享的 assembler。[核心主干](core.md)在每个轮次持有并记录这些值;本页声明它们。 + +源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + + + +## 内容块与消息 + +一段对话由 `Message` 组成;一条消息是一个类型化**内容块**的数组。块的联合类型从 `ContentBlockMap` 派生。 + +源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + +```ts type-equiv +/** + * Merge-extensible content blocks keyed by `type`. New core blocks must land + * with adapter, UI, and compaction support. + */ +interface ContentBlockMap { + 'text': TextBlock + 'reasoning': ReasoningBlock + 'tool-call': ToolCallBlock + 'tool-result': ToolResultBlock +} +``` + +各块接口(完整字段见源码):`TextBlock`(`text`)、`ReasoningBlock`(thinking,区别于可见文本)、`ToolCallBlock`(`id: CallId`、`name`、原始 JSON `arguments`)、`ToolResultBlock`(`toolCallId`、嵌套 `content: ContentBlock[]`、`isError?`)。`ContentBlock = ContentBlockMap[ContentBlockType]`。核心集仅限于每条交付路径都尊重的块——多模态内容(图像、音频等)没有核心块类型;需要的功能通过可合并扩展的 map 添加,同时提供适配器/UI/压缩支持。 + +源码:[`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts) + +`Message` 是一个带标识且不可变的角色/来源/内容值。模型产生的 assistant 消息会在其来源中携带提供方/模型所有权与可选的适配器私有回放元数据: + +```ts type-equiv +/** Provider ownership and adapter-private replay data for an assistant message. */ +interface AssistantProvenance { + /** Provider route that produced the message. */ + provider: string + /** Provider model id that produced the message. */ + model: string + /** + * Lossless-JSON adapter state needed to replay the provider response. + * `LlmService` exposes it to a target adapter only when that adapter instance + * currently owns both this historical provider and the target provider. + */ + replayState?: unknown +} +``` + +```ts type-equiv +/** One immutable message representation shared by delivery, durable history, and model requests. */ +interface Message { + /** Stable identity preserved across every representation boundary. */ + readonly id: MessageId + /** Provider-neutral conversation role. */ + readonly role: 'system' | 'user' | 'assistant' + /** Exact model-facing blocks. */ + readonly content: ContentBlock[] + /** Required producer provenance. */ + readonly source: MessageSource +} +``` + +消息来源本身也是一个可合并扩展的和类型: + +```ts type-equiv +/** + * Where a message (or injected content) came from. + * Merge-extensible sum type — plugins add their own `kind`s. + */ +interface MessageSourceMap { + user: { kind: 'user' } + plugin: { kind: 'plugin'; plugin: string } & ContextFormed + model: ModelMessageSource + tool: ToolMessageSource +} +``` + +溯源与形态是相互独立的两根轴。`kind` 回答「由谁产生」;生产方可选混入的 `form` 回答「这是何种形态的信息」,因此多个生产方可以共用一种呈现,一个生产方在一次会话中也可以发出多种形态。该词汇表是语义的,逐个取值增长;未声明或无法识别的取值是有文档的默认,按不透明内容呈现: + +```ts type-equiv +/** + * What SHAPE of information a producer-supplied context carries, declared by + * the producer beside its provenance. + * + * `MessageSource.kind` answers *who produced this*; `form` answers *what kind + * of thing it is*, and the two axes are deliberately independent — several + * producers share one form (three snapshot producers today), and one producer + * may emit more than one form over a session. + * + * The vocabulary is SEMANTIC, never visual: a value states that the content is + * a file's instructions or a catalog of available items, and a consumer decides + * what that looks like. Colors, icons, ordering, and collapse defaults are the + * consumer's business and must not enter this union. It grows one value at a + * time as producers gain the structured fields their form needs; an absent or + * unknown value is the documented default, presented as opaque content. + */ +type ContextForm = + /** Instructions read out of workspace files the model is expected to follow. */ + | 'instructions' + /** A catalog of items available in this session, republished as it changes. */ + | 'catalog' + /** Current state, where a later snapshot from the same producer supersedes an earlier one. */ + | 'snapshot' + /** A one-off account of something that just happened; it supersedes nothing. */ + | 'notice' + /** A message another agent addressed to this one. */ + | 'relay' + /** Material lifted out of another session's log, possibly reduced on the way in. */ + | 'recall' +``` + +```ts type-equiv +/** One named contribution to a `snapshot`-form context, in assembly order. */ +interface ContextSnapshotSection { + /** The contributing subsystem's name. */ + readonly name: string + /** That contribution's model-facing text, exactly as assembled. */ + readonly text: string +} +``` + +```ts type-equiv +/** + * Producer-declared {@link ContextForm} and the fields that form requires, + * mixed into the source shapes that carry one. + * + * Discriminated by `form` so a producer cannot declare a shape without the + * facts that shape is presented from: a `notice` must record its one-line + * account, a `snapshot` its sections. Omitting `form` stays valid — an + * undeclared context is the documented default. + */ +type ContextFormed = + | { readonly form?: never } + | { readonly form: 'instructions' } + | { readonly form: 'catalog' } + | { + readonly form: 'snapshot' + /** The named contributions this snapshot assembled, in order. */ + readonly sections: readonly ContextSnapshotSection[] + } + | { + readonly form: 'notice' + /** One-line account of what happened, shown without expanding the row. */ + readonly summary: string + } + | { readonly form: 'relay' } + | { readonly form: 'recall' } +``` + + + +## `StreamChunk`:原始协议 + +一个流式响应交错包含多种类型的块(文本、推理(reasoning)、多个工具调用)。`index` 将每个 delta 关联到其所属块;`block-end` 携带完整组装好的 `ContentBlock`,消费方无需自行重新组装 delta。这是一个**封闭的**可辨识联合类型:对 `type` 的 `switch` 以 `assertNever` 结尾,因此新增变体会在每个必须处理它的消费方处触发编译错误。 + +```ts type-equiv +/** + * Raw streaming protocol emitted by adapters. + * Block indexes correlate interleaved deltas, and `block-end` carries the + * assembled block. Adapters emit usage before the terminal finish and nothing + * afterward; tool arguments remain raw JSON strings. An adapter implementation + * may throw, but `LlmService.stream()` normalizes that failure to a terminal + * `error` or `aborted` finish before exposing it to consumers. + */ +type StreamChunk = + | { type: 'block-start'; index: number; blockType: ContentBlockType } + | { type: 'text-delta'; index: number; text: string } + | { type: 'reasoning-delta'; index: number; text: string } + | { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string } + | { type: 'block-end'; index: number; block: ContentBlock } + | { type: 'usage'; usage: TokenUsage } + | { + type: 'finish' + reason: FinishReason + /** Adapter-private lossless-JSON state for replaying a successful response. */ + replayState?: unknown + } +``` + + + +## `LlmFailure` + +每个抛出的失败或最终适配器的带内失败都会规范化为一种可序列化、提供方无关的 payload。`providerRetryAfterMs` 是经校验、由提供方请求的正数延迟,而不是重试决策;`ProviderRequestId` 是用于诊断的不透明品牌字符串。 + +```ts type-equiv +/** Serializable provider-boundary facts; policy decides whether they are retryable. */ +interface LlmFailure { + /** Human-readable provider or transport failure. */ + readonly message: string + /** Stable provider-neutral machine-routing code. */ + readonly code: string + /** HTTP status observed at the provider boundary, when available. */ + readonly status?: number + /** Provider-requested delay in milliseconds, when valid and available. */ + readonly providerRetryAfterMs?: number + /** Opaque provider-issued request identifier for diagnostics. */ + readonly requestId?: ProviderRequestId +} +``` + +## 适配器契约 + +每个适配器必须遵守以下规则,每个消费方可以依赖它们: + +- **`usage` 在 `finish` 之前,`finish` 之后不再有任何分片。** 将两者都推迟到提供方的流结束标记,这样尾部的 usage-only 分片就不会违反顺序。 +- **工具调用的 `arguments` 全程保持原始 JSON 字符串。** 部分片段通过 `argumentsDelta` 流式传输;如果提供方返回的是已解析的对象,适配器在 `block-end` 时重新序列化为字符串。 +- **两条受支持的错误路径,一种事实形状。** 失败可以从 `stream()` 抛出(传输/协议错误),**或者**以 `finish {kind:'error'|'aborted', failure}` 结束流(无法在流中途抛异常的适配器用它表示提供方带内错误)。`LlmError.failure` 携带同一个 `LlmFailure`。最终适配器边界保留被抛出的确切 `Error` 对象,并将不可变事实以及实际服务注册所对应的不可变重试策略关联到该调用;agent loop(智能体循环)关闭失败步骤,再把错误、事实、不可变的先前已重试失败事实、实际服务策略和轮次信号提供给 `agent/request-error`。处理该错误的 listener 在其 await 的修复完成后返回 `{ kind: 'retry' }`;若未恢复,结构化失败会成为轮次错误,并且该次尝试不会提交正常 assistant 消息或工具副作用。 +- **一次适配器调用就是一次提供方尝试。** 适配器禁用库重试。agent 层恢复会打开另一个持久、带编号的轮次;直接调用 `ctx.llm.stream()` 的调用方仍然只尝试一次。 +- **提供方停顿在传输层受到时限约束。** 两个已交付的远程适配器都暴露正数且有限的 `streamIdleTimeoutMs`,默认五分钟。watchdog 只在 iterator `next()` 尚未完成时启动,整个请求使用同一个稳定 signal,把自身到期映射为 `TIMEOUT`,并把更早发生的调用方中止保留为 `ABORTED`。 +- **上下文溢出只有一个规范 code。** 两个 DeepSeek 适配器都通过 `isContextWindowExceededError()` 对提供方的显式细节分类并暴露 `CONTEXT_WINDOW_EXCEEDED`,无论失败以抛出的 HTTP `LlmError` 还是带内 finish error 到达。消费方按 code 路由,绝不依赖提供方文本。 +- **空 completion 是可重试错误,而不是静默的成功结果。** 两个适配器都把没有携带任何内容块的终止性 `stop` 结束映射为携带规范 `EMPTY_RESPONSE` code 的 `finish {kind:'error'}`,`dsh-llm-retry` 默认会重试它;详见[空模型响应可重试](../../.agents/notes/implemented/bug-fix/2026-07-24-empty-model-response-is-retryable.md)。 +- **每个提供方 HTTP 请求都携带应用归属头。** 适配器发送 `attributionHeaders()`(见下文)作为 `User-Agent` 基线,并通过协议级测试加以证明(mock 服务器断言收到的 header,或对基于库的适配器使用库的 header 钩子)。 +- **回放状态归适配器所有。** 成功的 `finish` 可以携带重建提供方原生响应所需的无损 JSON 状态。循环会将其与组装后的 assistant 消息一起存储。后续请求中,仅当历史提供方与目标提供方当前注册到完全相同的适配器实例时,`LlmService` 才会传递该状态。该适配器负责校验状态并拥有所有跨模型或跨提供方转换;其他适配器只会收到提供方无关的内容与 provenance,不会收到私有状态。 + +该契约由两个有意保持独立的实现锁定:`dsh-llm-deepseek`(直接 fetch,SSE(Server-Sent Events)分帧经由 `eventsource-parser`)和 `dsh-llm-pi-ai`(通过 `@earendil-works/pi-ai` 实现的通用多提供方适配器)。基于库的适配器覆盖 finish 分片错误路径,而传输边界测试证明每个空闲 watchdog 都会停止其实际请求。 + +## `ResolvedRetryPolicy` + +提供方配置会在路由注册前解析为不可变的可辨识联合。normal mode 携带 `mode: 'normal'`、有限的 `maxRetries`、`retryableCodes`,以及必填的 `initialDelayMs`、`maxDelayMs` 与 `jitterRatio`;always mode 携带 `mode: 'always'` 和相同的必填退避字段,但没有有限上限。`LlmService.providerRetryPolicy(provider)` 返回当前注册的值,并在适配器省略策略时提供 normal 默认值;调用进入最终适配器边界后,`llmRetryPolicyOf(stream)` 返回为其提供服务的确切注册所捕获的值,因此之后释放或替换路由都无法改变进行中失败的恢复策略。可选输入形状由[生成的配置目录](../config-catalog.md)规定。 + +## `AppIdentity`:应用归属 + +每个适配器都会向提供方发送的静态公开应用标识([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts))。`attributionHeaders(identity?)` 只把它映射到标准 `User-Agent` header;该契约有意不支持 OpenRouter 特有的应用归属 header。默认 `APP_IDENTITY` 从包(package) manifest(元数据清单)获取版本;每个字段都是公开产品事实——不含 secret、路径、会话 id 或逐用户标识,且任何逐请求信息都不得影响这些值。设计理由见[强制 `User-Agent` 归属](../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。 + +```ts type-equiv +/** + * Static public application identity sent to LLM providers. + * + * Every field is a public product fact, safe on every request: no secrets, + * local paths, session ids, prompt text, or per-user identifiers belong here, + * and nothing per-request may influence the values. + */ +interface AppIdentity { + /** `User-Agent` product token (lowercase, hyphenated). */ + product: string + /** Product version; sourced from package metadata, never hand-copied. */ + version: string + /** Public home URL of the app, used as the `User-Agent` comment. */ + url: string +} +``` + + + +## `TokenUsage` + +逐调用 token 记账。各计数**互不重叠**:`inputTokens` 只包含未缓存输入;缓存输入单独报告,计费输入是三者之和。若提供方把缓存命中折入单一提示词总数(如 DeepSeek 的 `prompt_tokens`),适配器会再将其扣除。`reasoningTokens` 存在时只是信息性细节,已经包含在 `outputTokens` 中;汇总时不得重复相加。 + +```ts type-equiv +/** + * Token accounting for one model call (cache fields are optional). + * + * Counts are DISJOINT: `inputTokens` is uncached input only; cached input is + * reported separately as `cacheReadTokens`/`cacheWriteTokens` (billed input = + * sum of the three). Adapters whose providers fold cache hits into a total + * prompt count (DeepSeek's `prompt_tokens`) subtract them out. + */ +interface TokenUsage { + inputTokens: number + outputTokens: number + cacheReadTokens?: number + cacheWriteTokens?: number + reasoningTokens?: number +} +``` + + + +## `BlockAssembler` + +`BlockAssembler`([`packages/llm/llm/src/assembler.ts`](../../packages/llm/llm/src/assembler.ts))是唯一的共享实现,负责把 `StreamChunk` 流折叠回 `ContentBlock`、usage、结束原因与回放状态。循环在记录原始分片的同时,把同一批分片送入 assembler,再将组装后的 assistant 内容连同其提供方/模型 provenance 一起存储。需要组装结果、又不想重新实现 fold 的消费方使用它。 + +```ts public-api +/** + * Incrementally assembles raw {@link StreamChunk}s into complete + * {@link ContentBlock}s and a final assistant {@link Message}. + * + * The agent loop feeds it while logging raw chunks for replay fidelity, then + * reads `blocks()` / `message()` / `usage` / `finish` once the stream ends. + * + * Tolerant of delta-only protocols (no block-start/end); deltas arriving for + * an index already closed by `block-end` are ignored (malformed stream) so a + * misbehaving adapter cannot grow memory or corrupt a completed block. + */ +declare class BlockAssembler { + /** + * Feed one chunk into the assembly state. + * @param chunk - the next raw chunk, in stream order. + */ + push(chunk: StreamChunk): void; + /** + * Assemble all blocks seen so far, in stream order. + * @returns one block per seen index, except that max-token truncation drops + * tool calls that cannot be executed safely; an open block assembles from + * its accumulated deltas (an unknown block type never closed by `block-end` throws). + */ + blocks(): ContentBlock[]; + /** Usage from the `usage` chunk; undefined until one arrives. */ + get usage(): TokenUsage | undefined; + /** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */ + get finish(): FinishReason; + /** Adapter-private replay state from the terminal finish chunk, if any. */ + get replayState(): unknown; + /** + * The assembled assistant message. + * @param source - producer attribution for the assembled message. + * @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules). + */ + message(source: MessageSource = { kind: 'plugin', plugin: 'dsh-llm/assembler' }): Message; +} +``` + + + +## 模型请求 + +一次模型调用是一个完全组装好的 `GenerateOptions`。适配器以原始 [`StreamChunk`](#streamchunk--the-raw-protocol) 流作答;消费方用 [`BlockAssembler`](#blockassembler) 组装它。 + +源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + +提供方与模型发现使用小型、提供方无关的描述符。模型目录仅供参考:路由仍以已注册提供方为键,适配器也可以接受未列出的模型 id。 + +注册适配器会返回一个句柄:既是释放器,也带有原子的路由替换——路由集合由用户配置决定的插件正需要它。 + +```ts type-equiv +/** + * What {@link LlmService.registerAdapter} returns: the disposer, plus an + * atomic route replacement for the same adapter instance. + */ +interface AdapterRegistrationHandle { + /** Release every route this registration currently holds. */ + (): void + /** + * Replace this registration's routes with `providers`, keeping the same + * adapter instance. The candidate set is validated in full first — a + * conflict with another adapter, an invalid name, or bad provider metadata + * throws and leaves the current routes untouched — and the swap itself is + * one synchronous section, so no request can observe a gap. An empty array + * is legal here (a settings section that emptied holds zero routes while + * staying registered), unlike an empty initial registration. + * + * Throws `LlmError` with code `REGISTRATION_DISPOSED` once the registration + * has been released: its routes are gone and its disposer has already run, + * so anything registered afterwards would have no owner left to release it. + * @param providers - the complete next route set for this registration. + */ + replace(providers: string[]): void +} +``` + +```ts type-equiv +/** Display metadata for one registered provider route. */ +interface LlmProviderInfo { + /** Provider route key used by {@link GenerateOptions.provider}. */ + id: string + /** Human-readable provider name for selectors and diagnostics. */ + name: string +} +``` + +适配器插件还会通过 `registerConfigurableProviders()` 声明哪些路由*可以*运行,并指明每条路由的用户设置分节,使配置界面能在任何路由注册之前就呈现休眠的提供方。 + +```ts type-equiv +/** + * One provider route an adapter plugin can activate through configuration, + * whether or not the route is currently registered. Configuration surfaces + * merge this directory with `listProviders()` to offer every configurable + * provider alongside its live/dormant state. + */ +interface LlmConfigurableProvider { + /** Provider route key this entry activates when configured. */ + provider: string + /** Human-readable provider name for configuration surfaces. */ + displayName: string + /** User-settings namespace whose section configures this provider. */ + settingsNs: string + /** + * Path from that namespace's section root to this provider's profile + * object; empty when the whole section is the profile. + */ + settingsPath: readonly string[] + /** + * Whether the owning adapter knows this route only because configuration + * declared it — a gateway or self-hosted server it ships nothing about. + * Absent means the adapter draws no such distinction; false means it does + * and this route is one of its own. Only the adapter can answer: a stored + * profile is how a user-added route AND a corrected shipped one both look + * from outside. + */ + declared?: boolean +} +``` + +```ts type-equiv +/** One adapter-discovered model; catalog membership is advisory, not request validation. */ +interface LlmModelInfo { + /** Provider route that owns this model entry. */ + provider: string + /** Model id passed to {@link GenerateOptions.model}. */ + id: string + /** Human-readable model name for selectors. */ + name: string + /** Optional user-facing distinction from otherwise similar models. */ + description?: string +} +``` + +对正确性敏感的元数据与参考目录分开解析,并归服务该确切路由的适配器所有。上下文容量、适配器调用默认值和推理选项共用同一个确切模型结果,消费方因而无需重复执行权威模型解析。 + +```ts type-equiv +/** Provider-owned context capacity for one exact provider/model route. */ +interface LlmModelContext { + /** Maximum combined request and response context in tokens. */ + contextWindow: number +} +``` + +推理强度是另一项针对确切路由的能力。核心为标识符添加品牌类型,但不枚举其值;有序集合、展示名称和可选的部署默认值均由各适配器持有。 + +```ts type-equiv +/** Adapter-owned identifier for one model's selectable reasoning effort. */ +type ReasoningEffortId = Branded<'ReasoningEffortId'> +``` + +```ts type-equiv +/** Display metadata for one adapter-owned reasoning effort. */ +interface LlmReasoningEffortInfo { + /** Opaque stable value accepted by {@link GenerateOptions.reasoningEffort}. */ + id: ReasoningEffortId + /** Human-readable effort name for selectors and diagnostics. */ + name: string + /** Optional user-facing distinction from otherwise similar efforts. */ + description?: string +} +``` + +```ts type-equiv +/** Selectable reasoning efforts for one exact provider/model route. */ +interface LlmModelReasoningInfo { + /** Supported efforts in adapter-preferred display order. */ + efforts: readonly LlmReasoningEffortInfo[] + /** + * Adapter-configured default materialized into requests when callers omit + * an effort. Absence preserves the provider's own default. + */ + defaultEffort?: ReasoningEffortId +} +``` + +```ts type-equiv +/** Exact-route model metadata resolved by its owning adapter. */ +interface LlmResolvedModelInfo extends LlmModelInfo { + /** Provider-owned context capacity when known. */ + context?: LlmModelContext + /** Adapter-configured per-request output cap materialized when callers omit one. */ + defaultMaxTokens?: number + /** Adapter-owned selectable reasoning levels when exposed. */ + reasoning?: LlmModelReasoningInfo +} +``` + +```ts type-equiv +/** A single model request, fully assembled. */ +interface GenerateOptions { + /** Registered provider route selecting the adapter instance. */ + provider: string + model: string + /** Adapter-owned reasoning effort selected for this exact model. */ + reasoningEffort?: ReasoningEffortId + /** + * Ordered conversation messages, exactly as the provider sees them (after + * the `system` slot). A loop-built request assembles them as + * the derived history (dsh-agent-loop); a hand-built one-shot passes any list. + */ + messages: Message[] + /** System prompt text (adapters map to the provider's system slot). */ + system?: string + /** Tool schemas (adapters map to the provider's `tools` field). */ + tools?: ToolSchema[] + temperature?: number + maxTokens?: number + /** + * Stop sequences: generation halts as soon as the model produces any one of + * these strings (adapters map to the provider's stop field, e.g. OpenAI + * `stop`). The stop string itself is not included in the output. + */ + stop?: string[] + signal?: AbortSignal + /** + * Session identity stamped by the loop for listener routing. Adapters ignore + * it; replay uses it to keep concurrent parent and child cursors independent. + */ + sessionId?: Branded<'SessionId'> + /** + * Provider-neutral classification for an auxiliary model call. Adapters may + * map the purpose to model-hidden transport metadata or purpose-specific + * generation policy. Ordinary conversation requests leave it unset. + */ + purpose?: 'compaction' | 'session-title' +} +``` + +模型响应为何停止由可合并扩展的原因表示。提供方终态失败携带流式契约的 [`LlmFailure`](#llmfailure): + +```ts type-equiv +/** + * Why a model response stopped. + * Merge-extensible so adapters can surface provider-specific reasons. + */ +interface FinishReasonMap { + 'stop': { kind: 'stop' } + 'tool-calls': { kind: 'tool-calls' } + 'max-tokens': { kind: 'max-tokens' } + 'aborted': { kind: 'aborted'; failure: LlmFailure } + 'error': { kind: 'error'; failure: LlmFailure } +} +``` + +`FinishReason = FinishReasonMap[keyof FinishReasonMap]`。`TokenUsage`(逐调用计量,含不相交的缓存字段)详见[下文](#tokenusage)。 + +`GenerateOptions.tools` 携带 `ToolSchema`——工具的 JSON Schema 描述,发送给模型。它声明在 dsh-llm(而非 dsh-tools)中,正是因为它是循环每一步组装请求的一部分: + +```ts type-equiv +/** + * JSON-schema description of a tool, as sent to the model. + * + * Declared here (not in dsh-tools) because it is part of {@link GenerateOptions}; + * dsh-tools' ToolDefinition and dsh-system-prompt's PromptAssembly both import + * it from this package. + */ +interface ToolSchema { + name: string + description: string + /** JSON Schema object for the arguments. */ + parameters: Record +} +``` + +面向模型的 `ToolSchema` 是协议格式;产出它的已注册 `ToolDefinition`(schema + `execute`)在 [tools.md](tools.md) 中。 + +界面正在起草的提供方既没有路由也没有 catalog,因此询问被单独描述:请求携带用户正在编辑的草稿,回复是界面可以采纳的候选,而不是它必须服务的 catalog。 + +```ts type-equiv +/** + * One interrogation of a provider endpoint that configuration has not stored + * yet. Configuration surfaces send the draft a user is still editing, so the + * request carries the endpoint and credential directly instead of naming a + * route: a provider being added has no route to name. + */ +interface LlmModelDiscoveryRequest { + /** + * Route the draft is editing, when it edits an existing one. A route whose + * adapter already knows its models answers from that knowledge instead of + * asking the endpoint — the adapter's own registry is the better answer, and + * it costs no network call. + */ + provider?: string + /** + * Endpoint to interrogate. Optional because a route the adapter already + * describes needs none; a route it does not must supply one. + */ + baseURL?: string + /** Wire protocol the endpoint speaks, when the draft names one. */ + api?: string + /** Credential for this interrogation alone; the harness never stores it. */ + apiKey?: string + /** Caller cancellation; implementations must settle promptly after it aborts. */ + signal?: AbortSignal +} +``` + +```ts type-equiv +/** + * One model an endpoint reports about itself. Every field but the id is + * optional because most provider listings disclose an id and nothing else; + * a surface adopting one of these still owes the capacities its adapter needs. + */ +interface LlmDiscoveredModel { + /** Model id the endpoint accepts. */ + id: string + /** Human-readable name when the endpoint supplies one. */ + name?: string + /** Maximum combined request and response context, when disclosed. */ + contextWindow?: number + /** Maximum output tokens, when disclosed. */ + maxTokens?: number +} +``` + +### 请求信封:`LlmCallConfig` 与记录的 header + +循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、适配器默认值来源、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。 + +`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置及其来源。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。 + +在协议格式上,循环构建的请求先读取 `system` 槽位(渲染后的提示词组装),再读取派生历史——边界快照,其尾部在轮次首步是最新的 `user/message`,在后续步骤是上一步的工具结果。开发不变式针对每个循环构建的请求精确重算此等式。 + +FIXME(call-config-shape):重新审视其余哪些字段出于缓存目的确实属于 epoch 层级(`model` 和模型持有的推理强度已明确属于;采样标量目前出于谨慎保留在此)。 + +```ts type-equiv +/** + * Provider, model, reasoning effort, and sampling scalars of one conversation's + * requests. Every field maps 1:1 onto the same-named `GenerateOptions` field; + * the loop builds requests from the logged header rather than accepting these + * per call. + */ +interface LlmCallConfig { + provider: string + model: string + reasoningEffort?: ReasoningEffortId + temperature?: number + maxTokens?: number + stop?: string[] +} +``` + +```ts type-equiv +/** + * Effective config fields supplied by exact-model adapter resolution rather + * than by the caller's request proposal. + */ +interface LlmCallConfigAdapterDefaults { + reasoningEffort?: true + maxTokens?: true +} +``` + +## seam + +`LlmAdapter` 是提供方 seam:创建子类、实现 `stream()`,再用 `ctx.llm.registerAdapter(providers, adapter)` 注册一个适配器实例。`GenerateOptions.provider` 选择已注册适配器;`GenerateOptions.model` 会传给该适配器,无需在生命周期启动时注册。重复提供方路由会原子失败。可选的 `providerRetryPolicy()` 会按路由捕获并填入 normal 默认值,`providerInfo()` 与异步 `listModels()` 方法则为 `LlmService.listProviders()` / `listModels()` 提供分离的 selector 元数据。该目录仅供参考,不是请求白名单:适配器仍是权威,并可接受未列出的模型 id。单次异步 `resolveModel()` 查询返回确切模型身份,以及可选的对正确性敏感的上下文容量、适配器配置的 `defaultMaxTokens`、由模型持有的有序推理强度 ID 和部署默认值;字段缺失表示元数据不可用或保留提供方持有的行为,而不表示目录成员关系无效。解析器会接收可选的取消信号,并且必须在信号中止后迅速完成结算。`LlmService.resolveModelInfo()` 会校验聚合结果并返回分离值。在最终适配器边界,`resolveCallConfig()` 仅在 `maxTokens` 缺失时填入输出默认值,并校验和填入推理强度,因此直接调用也无法绕过任何一项已配置行为;直接分派会在等待解析前捕获一项适配器注册。agent loop 则使用 `prepareCall()`,使模型解析、请求头持久记录和分派全程使用同一项注册,保留来自同一次查询的分离上下文元数据,并报告适配器填入的配置字段。适配器查找发生在 `llm/stream` waterfall(瀑布式事件)的终端 continuation,因此 listener 可以在查找前短路调用,或路由一个可变的一次性请求。AgentLoop 在外层 waterfall 返回流句柄时观察到一次请求尝试;这个有限边界不能证明惰性终端适配器已构造完成或开始提供方 I/O。`block-start` / `block-end` 的 `index` 关联与 assembler 共同意味着适配器只需 emit 格式正确的分片——块重组不是每个适配器各自的问题。消费方 surface(`ctx.llm.stream()`)与 `llm/stream` waterfall 见 [architecture.md § 内容块与流式传输](../architecture.md#content-blocks-and-streaming-dsh-llm)。 + +```ts type-equiv +/** One model call whose config and adapter registration were resolved together. */ +interface PreparedLlmCall { + /** Detached, deep-frozen config with any adapter-owned default materialized. */ + readonly config: LlmCallConfig + /** Immutable retry policy captured with the adapter registration. */ + readonly retryPolicy: ResolvedRetryPolicy + /** Detached context metadata resolved with the registration-bound call. */ + readonly context?: LlmModelContext + /** Config fields materialized by the captured adapter rather than proposed by the caller. */ + readonly adapterDefaults: LlmCallConfigAdapterDefaults + /** + * Dispatch this call once through the registration captured during + * preparation. The request's call-config fields must match {@link config}; + * reuse or mismatch fails with `INVALID_PREPARED_CALL`. + * @param options - fully assembled request carrying the prepared config. + * @returns the chunk stream, including the `llm/stream` waterfall. + */ + stream(options: GenerateOptions): AsyncIterable +} +``` + +```ts public-api +/** + * Provider-wire adapter for the harness message and stream vocabulary. Register implementations + * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include + * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The direct-fetch + * DeepSeek and library-backed pi-ai adapters intentionally exercise this contract through different internals. + */ +declare abstract class LlmAdapter { + /** + * Describe one provider route owned by this adapter. + * @param provider - a route passed to `registerAdapter()` for this instance. + * @returns detached display metadata whose id must equal `provider`. + */ + providerInfo(provider: string): LlmProviderInfo; + /** + * Return the provider-owned retry policy captured with this route. + * @param _provider - a route passed to `registerAdapter()` for this instance. + * @returns a resolved policy, or `undefined` to use the normal defaults. + */ + providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined; + /** + * List models this adapter can currently advertise for one owned provider. + * The result is advisory: an adapter may accept unlisted model ids, and + * consumers must not turn absence into request rejection. + * @param _provider - one provider route owned by this adapter. + * @returns discoverable models in adapter-preferred order. + */ + listModels(_provider: string): Promise; + /** + * Resolve all metadata available for one exact model. This query is + * independent of the advisory catalog and does not validate request routing. + * @param provider - one provider route owned by this adapter. + * @param model - exact model id passed to {@link GenerateOptions.model}. + * @param _signal - cancellation for this exact-model lookup; asynchronous + * implementations must settle promptly after it aborts. + * @returns provider/model identity plus any context, call-default, and reasoning metadata. + */ + resolveModel( + provider: string, + model: string, + _signal?: AbortSignal, + ): Promise; + /** + * Stream one model call as raw chunks. The only required method. + * @param options - the fully-assembled request; implementations must honor `options.signal`. + * @returns the chunk stream, obeying the adapter contract documented on `StreamChunk`. + */ + abstract stream(options: GenerateOptions): AsyncIterable; +} +``` + +`ContentBlockType`(带 `index` 关联的块所携带的键集合)从上文的 [`ContentBlockMap`](#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 +``` + +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 +``` + +Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts) + diff --git a/docs/core-data-structures/lsp.i18n.yaml b/docs/subsystems/lsp.i18n.yaml similarity index 100% rename from docs/core-data-structures/lsp.i18n.yaml rename to docs/subsystems/lsp.i18n.yaml diff --git a/docs/core-data-structures/lsp.md b/docs/subsystems/lsp.md similarity index 100% rename from docs/core-data-structures/lsp.md rename to docs/subsystems/lsp.md diff --git a/docs/core-data-structures/lsp.zh.md b/docs/subsystems/lsp.zh.md similarity index 100% rename from docs/core-data-structures/lsp.zh.md rename to docs/subsystems/lsp.zh.md diff --git a/docs/subsystems/permission.i18n.yaml b/docs/subsystems/permission.i18n.yaml new file mode 100644 index 0000000000..3f9c9b533c --- /dev/null +++ b/docs/subsystems/permission.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/permission.md +permission.md: 6f9a37dbb784ccbaea74056519992e40f9c07c6d +permission.zh.md: 8eda50fae09a67c41003d7a914fb139c4132a900 diff --git a/docs/subsystems/permission.md b/docs/subsystems/permission.md new file mode 100644 index 0000000000..6f9a37dbb7 --- /dev/null +++ b/docs/subsystems/permission.md @@ -0,0 +1,131 @@ +# Permission Presets + +English | [中文](permission.zh.md) + +The permission-preset layer of [dsh-permission](../../packages/interaction/permission) (`ctx.permission`, `PermissionService`) bundles the two independent enforcement knobs — [sandbox mode](sandbox.md) (`sandbox/mode`) and [approval policy](approval.md) (`approval/policy`) — into named presets a client offers as one Permissions selector. It is one optional capability, not part of the agent-loop spine, and it owns no enforcement: execution, prompt narration, and replay keep reading their knob folds, and a preset switch only records intent and writes through each knob's canonical setter. The [package README](../../packages/interaction/permission/README.md) owns composition status and limitations; the [sandbox switching design](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns the rationale. + +Source: [`packages/interaction/permission/src/index.ts`](../../packages/interaction/permission/src/index.ts) + +## The preset table + +A preset is a table key mapping to one sandbox/approval bundle plus optional client presentation; the default table ships `workspace-write` (`workspace-write` + `ask`) and `danger-full-access` (`danger-full-access` + `never`). + +```ts type-equiv +/** One preset's sandbox/approval bundle and optional client presentation. */ +interface PresetSpec { + /** The `sandbox/mode` value the preset writes through. */ + sandbox: SandboxMode + /** The `approval/policy` value the preset writes through. */ + approval: ApprovalPolicy + /** The display label a client shows for this preset; the raw table key when omitted. */ + name?: string + /** One user-facing sentence on what the preset means; omitted when not configured. */ + description?: string +} +``` + +```ts type-equiv +/** The {@link PermissionService} config: preset table and composition default. */ +interface Config { + /** + * The preset table: name → knob bundle. Defaults to `workspace-write` + * (workspace-write + ask) and `danger-full-access` (danger-full-access + + * never). The name `custom` is reserved for the derived not-a-preset state. + */ + presets?: Record + /** + * Default for new sessions. When omitted, the preset matching the composed + * sandbox and approval defaults is used. + */ + defaultPreset?: string +} +``` + +The service requires a confining `ctx.bash` executor and `ctx.approval`, and misconfiguration fails at plugin load: a table entry named `custom` throws (the name is reserved for the derived not-a-preset state), and composing over a bash executor that does not confine (no `sandboxMode` capability fact) throws, because presets bundle a sandbox mode. + +## Current preset and the derived `custom` + +`current(events)` derives the effective preset from the knobs, not from its own event alone: it folds the session's effective sandbox mode (falling back to the executor's configured mode) and effective approval policy (falling back to the approval service config, then `ask`), prefers a still-matching recorded selection, then the first matching table entry in declaration order, and otherwise returns `CUSTOM_PRESET` (`'custom'`). `custom` is derived-only: clients may display it as the current value, but it is never a switch target or an event payload. + +`names` lists the switchable presets in table declaration order; `optionOf(name)` builds the option a client renders for a table key (label falls back to the key) or for `custom`, and throws for any other name. + +```ts type-equiv +/** The select-option shape a presentation layer advertises for one preset (or for the derived `custom` state). */ +interface PresetOption { + /** Stable option value: the table key, or `custom`. */ + value: string + /** The display label. */ + name: string + /** One user-facing sentence on what the value means; omitted when not configured. */ + description?: string +} +``` + +## Switching and the `permission/preset` event + +`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](#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](session.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 new file mode 100644 index 0000000000..8eda50fae0 --- /dev/null +++ b/docs/subsystems/permission.zh.md @@ -0,0 +1,131 @@ +# 权限预设 + +[English](permission.md) | 中文 + +[dsh-permission](../../packages/interaction/permission) 的权限预设层(`ctx.permission`,`PermissionService`)把两个相互独立的强制执行旋钮(knob),即[沙箱模式](sandbox.md)(`sandbox/mode`)与[审批策略](approval.md)(`approval/policy`),捆绑成具名预设,供客户端作为单个权限(Permissions)选择器提供。它是一项可选能力,不属于 agent loop(智能体循环)主干,也不拥有任何强制执行:执行、提示词叙述与回放仍然读取各自旋钮的折叠结果,预设切换只记录意图,并通过每个旋钮各自的规范 setter 写入。[包(package)README](../../packages/interaction/permission/README.md) 负责组合状态与限制;[沙箱切换设计](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)负责决策依据。 + +源码:[`packages/interaction/permission/src/index.ts`](../../packages/interaction/permission/src/index.ts) + +## 预设表 + +预设是一个表键,映射到一个沙箱/审批组合,外加可选的客户端展示信息;默认预设表自带 `workspace-write`(`workspace-write` + `ask`)和 `danger-full-access`(`danger-full-access` + `never`)。 + +```ts type-equiv +/** One preset's sandbox/approval bundle and optional client presentation. */ +interface PresetSpec { + /** The `sandbox/mode` value the preset writes through. */ + sandbox: SandboxMode + /** The `approval/policy` value the preset writes through. */ + approval: ApprovalPolicy + /** The display label a client shows for this preset; the raw table key when omitted. */ + name?: string + /** One user-facing sentence on what the preset means; omitted when not configured. */ + description?: string +} +``` + +```ts type-equiv +/** The {@link PermissionService} config: preset table and composition default. */ +interface Config { + /** + * The preset table: name → knob bundle. Defaults to `workspace-write` + * (workspace-write + ask) and `danger-full-access` (danger-full-access + + * never). The name `custom` is reserved for the derived not-a-preset state. + */ + presets?: Record + /** + * Default for new sessions. When omitted, the preset matching the composed + * sandbox and approval defaults is used. + */ + defaultPreset?: string +} +``` + +该服务要求一个施加隔离的 `ctx.bash` 执行器和 `ctx.approval`,配置错误在插件加载时即失败:名为 `custom` 的表项会抛出异常(该名称保留给派生的「非预设」状态);在不施加隔离的 bash 执行器(没有 `sandboxMode` 能力事实)之上组合同样抛出异常,因为预设捆绑了一个沙箱模式。 + +## 当前预设与派生的 `custom` + +`current(events)` 从旋钮派生实际生效的预设,而不是只看自身事件:它折叠会话的生效沙箱模式(回退到执行器配置的模式)与生效审批策略(先回退到审批服务配置,再回退到 `ask`),优先取仍然匹配的已记录选择,其次取声明顺序中第一个匹配的表项,否则返回 `CUSTOM_PRESET`(`'custom'`)。`custom` 只是派生值:客户端可以把它显示为当前值,但它绝不是切换目标,也绝不出现在事件 payload 中。 + +`names` 按预设表声明顺序列出可切换的预设;`optionOf(name)` 为某个表键(label 回退为该键)或 `custom` 构建客户端渲染的选项,传入其他任何名称都会抛出异常。 + +```ts type-equiv +/** The select-option shape a presentation layer advertises for one preset (or for the derived `custom` state). */ +interface PresetOption { + /** Stable option value: the table key, or `custom`. */ + value: string + /** The display label. */ + name: string + /** One user-facing sentence on what the value means; omitted when not configured. */ + description?: string +} +``` + +## 切换与 `permission/preset` 事件 + +`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);方法签名见生成的[服务目录](#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](session.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 new file mode 100644 index 0000000000..7ca1d0f8fb --- /dev/null +++ b/docs/subsystems/persistence.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/persistence.md +persistence.md: aaeca4bbaad5ecd4535226e4b8a53ab8bbc75947 +persistence.zh.md: ac070ef60695a7469e50f9b24ed07259da5da4a3 diff --git a/docs/core-data-structures/persistence.md b/docs/subsystems/persistence.md similarity index 59% rename from docs/core-data-structures/persistence.md rename to docs/subsystems/persistence.md index aa95acfb4c..aaeca4bbaa 100644 --- a/docs/core-data-structures/persistence.md +++ b/docs/subsystems/persistence.md @@ -4,7 +4,7 @@ English | [中文](persistence.zh.md) The **durability seam** for the event log. [session.md](session.md) describes the in-memory `Session` — the append-only `SessionEvent` log that is the source of truth. This page describes how that log is made durable: the abstract `SessionPersistence` service, its backends, the flush checkpoint, crash recovery, and the metadata header that travels alongside the log. The event vocabulary the log carries is enumerated, member by member, in the generated [persistence log event catalog](../persistence-catalog.md). -The seam is a textbook [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): one abstract service ([dsh-session-persistence](../../packages/session-persistence/session-persistence), `ctx.sessionPersistence`) defining locate/create/append, reusable Session preparation, logical load/inspect, physical suffix reads, and lightweight list/snapshot observation over the existing `SessionEvent` — **no parallel persisted event type** — and two interchangeable backends implementing the same contract. See the [session-persistence Agent Note](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md). +The seam is a textbook [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): one abstract service ([dsh-session-persistence](../../packages/session/session-persistence), `ctx.sessionPersistence`) defining locate/create/append, reusable Session preparation, logical load/inspect, physical suffix reads, and lightweight list/snapshot observation over the existing `SessionEvent` — **no parallel persisted event type** — and two interchangeable backends implementing the same contract. See the [session-persistence Agent Note](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md). ## The flush checkpoint @@ -210,5 +210,137 @@ interface SessionPersistenceSnapshot { Both implement the same abstract `SessionPersistence` (locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots over `SessionEvent`, with optional cancellation on observation methods) and pass `runPersistenceContract`, proving the seam is genuinely backend-agnostic: -- **[dsh-session-persistence-jsonl](../../packages/session-persistence/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-persistence/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. +- **[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](session.md) · [SessionId](core.md) + +Source: [`packages/session/session-persistence/src/index.ts:72`](../../packages/session/session-persistence/src/index.ts) + diff --git a/docs/core-data-structures/persistence.zh.md b/docs/subsystems/persistence.zh.md similarity index 58% rename from docs/core-data-structures/persistence.zh.md rename to docs/subsystems/persistence.zh.md index e16420ee49..ac070ef606 100644 --- a/docs/core-data-structures/persistence.zh.md +++ b/docs/subsystems/persistence.zh.md @@ -4,7 +4,7 @@ 事件日志的**持久性 seam**。[session.md](session.md) 描述了内存中的 `Session`:仅追加的 `SessionEvent` 日志即为真源。本页描述如何使该日志持久化:抽象的 `SessionPersistence` 服务、它的后端、flush 检查点、崩溃恢复,以及随日志一同存储的元数据头。日志承载的事件词汇在生成的[持久化日志事件目录](../persistence-catalog.md)中逐项列举。 -该 seam 是典型的[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):一个抽象服务([dsh-session-persistence](../../packages/session-persistence/session-persistence),`ctx.sessionPersistence`)在现有 `SessionEvent` 上定义 locate/create/append、可复用的 Session 准备流程、逻辑 load/inspect、物理后缀读取,以及轻量的 list/snapshot 观察——**没有平行的持久化事件类型**——以及两个实现同一契约的可互换后端。见 [session-persistence Agent Note](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)。 +该 seam 是典型的[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):一个抽象服务([dsh-session-persistence](../../packages/session/session-persistence),`ctx.sessionPersistence`)在现有 `SessionEvent` 上定义 locate/create/append、可复用的 Session 准备流程、逻辑 load/inspect、物理后缀读取,以及轻量的 list/snapshot 观察——**没有平行的持久化事件类型**——以及两个实现同一契约的可互换后端。见 [session-persistence Agent Note](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)。 ## flush 检查点 @@ -210,5 +210,137 @@ interface SessionPersistenceSnapshot { 两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots,观察方法可选支持取消),并通过 `runPersistenceContract`,证明该 seam 确实与后端无关: -- **[dsh-session-persistence-jsonl](../../packages/session-persistence/session-persistence-jsonl)**——每个会话一份仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame,也可配置为原始行;支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。 -- **[dsh-session-persistence-sqlite](../../packages/session-persistence/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行结构 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。 +- **[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](session.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 new file mode 100644 index 0000000000..efe9e376c6 --- /dev/null +++ b/docs/subsystems/plan.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/plan.md +plan.md: ee4b911050863765960ba46952ba923a7115e129 +plan.zh.md: 0481bdf76adf2f00fe18073fe8daa5ba172bbfe1 diff --git a/docs/subsystems/plan.md b/docs/subsystems/plan.md new file mode 100644 index 0000000000..ee4b911050 --- /dev/null +++ b/docs/subsystems/plan.md @@ -0,0 +1,86 @@ +# Plan Mode + +English | [中文](plan.zh.md) + +Plan mode is logged per-agent collaboration state owned by [dsh-plan-mode](../../packages/plan/plan-mode) (`ctx.planMode`, `PlanModeService`): while active, a deployment-owned guidance section shapes each model request. It is **soft guidance**, deliberately independent of the [sandbox mode](sandbox.md) and [approval policy](approval.md) enforcement axes — those knobs never read or write plan state, and deployments needing a hard boundary combine them separately. The package is one optional capability, not part of the agent-loop spine; its surfaces are the `plan:policy` prompt section, the always-registered `exit_plan_mode` tool, and the `/plan` command. The [design note](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md) owns the rationale; the [package README](../../packages/plan/plan-mode/README.md) owns the model-experience and limitation detail. + +Source: [`packages/plan/plan-mode/src/index.ts`](../../packages/plan/plan-mode/src/index.ts) + +## Logged state and recovery + +`plan/mode` (`{ active: boolean }`) is a log-only, whole-value-replace [session event](session.md): durable and replayable, never in the model transcript. `foldPlanMode(events, end?)` returns the last logged value in the prefix, or `false` when there is none — the state in force is always a pure fold of the session log, so resume, fork, and compaction recover it with no live mirror, and UIs observe committed flips through `session/event`. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md). + +## Pending intent and the step-boundary flush + +Because every session event is turn-enclosed, a user selection is held as pending intent until the next step boundary — the next request derivation, in whichever turn it occurs (selection never forces continuation, so an intent recorded after a turn's final step lands in a later turn). `set(agent, active)` records the pending selection (a no-op when the target equals the logged-or-already-pending state), and `get(agent)` returns `{ active: boolean; pending?: boolean }` — the logged state shaping the current step, plus the optimistic selection awaiting a boundary. + +The sole flush point is a prepended `agent/step` listener — the loop's in-turn interception seam that runs before every request derivation, including turn 1 step 1 and request-recovery retries. Prompt admission itself never flushes: it happens pre-turn, where a `plan/mode` append would land outside any open turn, so a selection made at the prompt is landed by the first step boundary inside the turn it starts. The prepend means the flush runs before the downstream `agent/step` listener chain. A flush failure is contained — plan policy can never block a turn — and the failed append stays pending for a later boundary. A flushed user selection also narrates the switch as one plugin-sourced `user/message` notice, but only when the last logged request header described the other state, so the model is told exactly when its context changed and never redundantly. A pending selection made while idle is process-local and lost on exit before the next boundary ([README limitation](../../packages/plan/plan-mode/README.md#known-limitations-and-deferred-work)). + +## Configuration + +```ts type-equiv +/** Deployment-owned plan guidance. */ +interface PlanModeConfig { + /** Guidance rendered as the `plan:policy` prompt section while plan mode is active. */ + section: string +} +``` + +A missing, blank, or non-string `section` and any unknown key fail at plugin load rather than silently shaping nothing. While plan mode is active, the exact `section` text renders as the `plan:policy` [system-prompt section](system-prompt.md) at order 50; inactive plan mode contributes no text. + +## The exit tool and the `/plan` command + +[`exit_plan_mode`](../tool-catalog.md#deepseek-aidsh-plan-mode) stays registered while plan mode is inactive, so crossing the boundary changes only the prompt section, never the request tool catalog; execution outside plan mode fails. In plan mode it requires a complete markdown plan starting with a `#` heading and presents it for review through the [user-interaction seam](user-interaction.md). Approval returns `{ approved: true }` and records a silent (non-narrated) pending exit that flushes after the step — plan guidance holds for the rest of the assistant's tool batch, and the tool result itself narrates the transition. Keep-planning is a failed call carrying the user's feedback, so the model revises and presents again; a missing interaction channel and a service reload during review also fail the call rather than silently leaving plan mode. + +When [`ctx.commands`](commands.md) is composed, the plugin registers `/plan [off|message]`: bare `/plan` selects plan mode, any other non-empty message selects it and then submits the text through `agent.steer()` so it becomes the next step's ordinary logged user message under plan guidance, and the exact argument `off` selects inactive — which also cancels a not-yet-flushed pending entry before plan mode ever reaches a request. + +## 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](#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 new file mode 100644 index 0000000000..0481bdf76a --- /dev/null +++ b/docs/subsystems/plan.zh.md @@ -0,0 +1,86 @@ +# 计划模式 + +[English](plan.md) | 中文 + +计划模式是 [dsh-plan-mode](../../packages/plan/plan-mode) 拥有的、记录到日志的逐 agent(智能体)协作状态(`ctx.planMode`,`PlanModeService`):激活期间,一段部署持有的指引段落会影响每个模型请求。它是**软性指引**,有意独立于[沙箱模式](sandbox.md)与[审批策略](approval.md)这两条强制执行轴:那些旋钮(knob)从不读写计划状态,需要硬边界的部署另行组合两者。该包(package)是一项可选能力,不属于 agent loop(智能体循环)主干;它的对外表面是 `plan:policy` 提示词段落、始终保持注册的 `exit_plan_mode` 工具和 `/plan` 命令。[设计说明](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md)负责决策依据;[包 README](../../packages/plan/plan-mode/README.md) 负责模型体验与限制细节。 + +源码:[`packages/plan/plan-mode/src/index.ts`](../../packages/plan/plan-mode/src/index.ts) + +## 已记录状态与恢复 + +`plan/mode`(`{ active: boolean }`)是仅记日志、整值替换的[会话事件](session.md):持久且可回放,绝不进入模型 transcript(文本记录)。`foldPlanMode(events, end?)` 返回前缀中最后一条已记录值,没有时返回 `false`:生效状态始终是会话日志的纯折叠,因此恢复、fork 与压缩(compaction)无需实时镜像即可将其复原,UI 通过 `session/event` 观察已提交的切换。完整事件声明见[持久化日志事件目录](../persistence-catalog.md)。 + +## 待定意图与步骤边界冲刷 + +由于每个会话事件都位于轮次之内,用户的选择会作为待定意图保留到下一个步骤边界——即下一次请求派生,落在哪个轮次就在哪个轮次生效(选择绝不强制续行,因此在某轮最后一步之后记录的意图会在之后的轮次落地)。`set(agent, active)` 记录待定选择(目标值与已记录或已在待定中的状态相同时不做任何事),`get(agent)` 返回 `{ active: boolean; pending?: boolean }`,即影响当前步骤的已记录状态,加上正在等待边界的乐观选择。 + +唯一的冲刷点是一个前置(prepend)注册的 `agent/step` 监听器——agent loop 的轮内拦截 seam,在每次请求派生之前运行,包括第 1 轮第 1 步和请求恢复重试。提示词提交本身绝不冲刷:它发生在轮次开启之前,此时追加 `plan/mode` 会落在任何开启的轮次之外,因此在提示词处做出的选择由它开启的轮次内的第一个步骤边界落地。前置注册意味着冲刷先于下游的 `agent/step` 监听器链运行。冲刷失败会被收容(计划策略绝不能阻塞轮次),追加失败的选择保持待定,等待后续边界。已冲刷的用户选择还会以一条插件来源的 `user/message` 通知叙述这次切换,但仅当最后记录的请求头描述的是另一种状态时才叙述,因此模型恰好在上下文变化时被告知,且绝不重复。空闲时做出的待定选择只存在于进程内,进程在下一个边界之前退出即丢失([README 限制](../../packages/plan/plan-mode/README.md#known-limitations-and-deferred-work))。 + +## 配置 + +```ts type-equiv +/** Deployment-owned plan guidance. */ +interface PlanModeConfig { + /** Guidance rendered as the `plan:policy` prompt section while plan mode is active. */ + section: string +} +``` + +`section` 缺失、为空白或不是字符串,以及任何未知键,都会在插件加载时失败,而不是静默地不产生任何指引。计划模式激活期间,确切的 `section` 文本以 order 50 渲染为 `plan:policy` [系统提示词段落](system-prompt.md);未激活的计划模式不贡献任何文本。 + +## 退出工具与 `/plan` 命令 + +[`exit_plan_mode`](../tool-catalog.md#deepseek-aidsh-plan-mode) 在计划模式未激活时仍保持注册,因此跨越边界只改变提示词段落,绝不改变请求的工具目录;在计划模式之外执行会失败。在计划模式中,它要求一份以 `#` 标题开头的完整 markdown 计划,并通过[用户交互 seam](user-interaction.md) 呈交评审。批准返回 `{ approved: true }`,并记录一个静默(不叙述)的待定退出,在该步骤之后冲刷:计划指引在 assistant 本批工具调用的剩余部分继续生效,而工具结果本身叙述这次转换。「继续规划」则是一次携带用户反馈的失败调用,模型据此修订并再次呈交;评审期间交互通道缺失或服务重载同样使调用失败,而不是静默离开计划模式。 + +当 [`ctx.commands`](commands.md) 被组合时,插件注册 `/plan [off|message]`:单独的 `/plan` 选择计划模式;任何其他非空消息先选择计划模式,再通过 `agent.steer()` 提交该文本,使其在计划指引下成为下一步骤的普通已记录用户消息;确切参数 `off` 选择未激活,这还会在计划模式尚未进入任何请求之前,取消尚未冲刷的待定条目。 + +## 服务 + +`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/core-data-structures/pty.i18n.yaml b/docs/subsystems/pty.i18n.yaml similarity index 57% rename from docs/core-data-structures/pty.i18n.yaml rename to docs/subsystems/pty.i18n.yaml index 11788e8e76..76b4500aea 100644 --- a/docs/core-data-structures/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/core-data-structures/pty.md b/docs/subsystems/pty.md similarity index 54% rename from docs/core-data-structures/pty.md rename to docs/subsystems/pty.md index 97e1e662d1..45b157c739 100644 --- a/docs/core-data-structures/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/core-data-structures/pty.zh.md b/docs/subsystems/pty.zh.md similarity index 54% rename from docs/core-data-structures/pty.zh.md rename to docs/subsystems/pty.zh.md index a57f744827..4409ef0b05 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..efbd158e18 --- /dev/null +++ b/docs/subsystems/sandbox.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/sandbox.md +sandbox.md: dd960b3021dcdc87cfd36fd439cbec0a810dd736 +sandbox.zh.md: f526982a782144a0bf58abcde5d883e9dc813369 diff --git a/docs/core-data-structures/sandbox.md b/docs/subsystems/sandbox.md similarity index 72% rename from docs/core-data-structures/sandbox.md rename to docs/subsystems/sandbox.md index 9e5feafe04..dd960b3021 100644 --- a/docs/core-data-structures/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/core-data-structures/sandbox.zh.md b/docs/subsystems/sandbox.zh.md similarity index 72% rename from docs/core-data-structures/sandbox.zh.md rename to docs/subsystems/sandbox.zh.md index a1314d1b78..f526982a78 100644 --- a/docs/core-data-structures/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/core-data-structures/scope.i18n.yaml b/docs/subsystems/scope.i18n.yaml similarity index 100% rename from docs/core-data-structures/scope.i18n.yaml rename to docs/subsystems/scope.i18n.yaml diff --git a/docs/core-data-structures/scope.md b/docs/subsystems/scope.md similarity index 100% rename from docs/core-data-structures/scope.md rename to docs/subsystems/scope.md diff --git a/docs/core-data-structures/scope.zh.md b/docs/subsystems/scope.zh.md similarity index 100% rename from docs/core-data-structures/scope.zh.md rename to docs/subsystems/scope.zh.md diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml new file mode 100644 index 0000000000..7b5da1c315 --- /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: 1df52b5afde0beb1d9eb5c3ad63af8c75ff5076f +session-projection.zh.md: 8754d590c4b9db71b4475185e26b8cafd9dd7b45 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md new file mode 100644 index 0000000000..1df52b5afd --- /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. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads. + +```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](session.md) + +Source: [`packages/session/session-projection/src/index.ts:171`](../../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..8754d590c4 --- /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. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads. + +```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](session.md) + +Source: [`packages/session/session-projection/src/index.ts:171`](../../packages/session/session-projection/src/index.ts) + diff --git a/docs/subsystems/session-query.i18n.yaml b/docs/subsystems/session-query.i18n.yaml new file mode 100644 index 0000000000..728a6cf8db --- /dev/null +++ b/docs/subsystems/session-query.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-query.md +session-query.md: 54ae1eaf34a17d4979a621e4fc310780b425364e +session-query.zh.md: b770514757c30aa5d87b25e5d280b1ce6710cd9c diff --git a/docs/core-data-structures/session-query.md b/docs/subsystems/session-query.md similarity index 66% rename from docs/core-data-structures/session-query.md rename to docs/subsystems/session-query.md index e7514dd6c3..54ae1eaf34 100644 --- a/docs/core-data-structures/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/core-data-structures/session-query.zh.md b/docs/subsystems/session-query.zh.md similarity index 66% rename from docs/core-data-structures/session-query.zh.md rename to docs/subsystems/session-query.zh.md index 4c3dd4d435..b770514757 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..c1466bdcfc --- /dev/null +++ b/docs/subsystems/session-reference.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-reference.md +session-reference.md: f539a59b8d26182aff9746b6d6a39a86ba15cb45 +session-reference.zh.md: 82b6a6c5ec5c91372fc6a43b73ad1572fccf5b3a diff --git a/docs/core-data-structures/session-reference.md b/docs/subsystems/session-reference.md similarity index 53% rename from docs/core-data-structures/session-reference.md rename to docs/subsystems/session-reference.md index 5375677f6a..f539a59b8d 100644 --- a/docs/core-data-structures/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](llm-streaming.md) + +Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts) + diff --git a/docs/core-data-structures/session-reference.zh.md b/docs/subsystems/session-reference.zh.md similarity index 53% rename from docs/core-data-structures/session-reference.zh.md rename to docs/subsystems/session-reference.zh.md index 3ff4a17199..82b6a6c5ec 100644 --- a/docs/core-data-structures/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](llm-streaming.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 new file mode 100644 index 0000000000..8b58415234 --- /dev/null +++ b/docs/subsystems/session-title.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-title.md +session-title.md: 3a2df284f2af14def284e502f4e37daaf4d7a262 +session-title.zh.md: 3a74baa9005459b35b658df95bdd7b57256b3e2e diff --git a/docs/core-data-structures/session-title.md b/docs/subsystems/session-title.md similarity index 62% rename from docs/core-data-structures/session-title.md rename to docs/subsystems/session-title.md index fff1aa1f6b..3a2df284f2 100644 --- a/docs/core-data-structures/session-title.md +++ b/docs/subsystems/session-title.md @@ -2,9 +2,9 @@ English | [中文](session-title.zh.md) -Durable latest-wins title state and the optional asynchronous provider vocabulary owned by [`@deepseek-ai/dsh-session-title`](../../packages/session-title/session-title). The shared LLM helper owns the exact auxiliary request record. Package READMEs own timing, fallback, failure, and fork behavior; the generated [persistence catalog](../persistence-catalog.md) owns the complete event declarations. +Durable latest-wins title state and the optional asynchronous provider vocabulary owned by [`@deepseek-ai/dsh-session-title`](../../packages/session/session-title). The shared LLM helper owns the exact auxiliary request record. Package READMEs own timing, fallback, failure, and fork behavior; the generated [persistence catalog](../persistence-catalog.md) owns the complete event declarations. -Sources: [`packages/session-title/session-title/src/index.ts`](../../packages/session-title/session-title/src/index.ts), [`packages/session-title/session-title-llm/src/index.ts`](../../packages/session-title/session-title-llm/src/index.ts) +Sources: [`packages/session/session-title/src/index.ts`](../../packages/session/session-title/src/index.ts), [`packages/session/session-title-llm/src/index.ts`](../../packages/session/session-title-llm/src/index.ts) ## Durable title state @@ -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/core-data-structures/session-title.zh.md b/docs/subsystems/session-title.zh.md similarity index 60% rename from docs/core-data-structures/session-title.zh.md rename to docs/subsystems/session-title.zh.md index 77a0a5e940..3a74baa900 100644 --- a/docs/core-data-structures/session-title.zh.md +++ b/docs/subsystems/session-title.zh.md @@ -2,9 +2,9 @@ [English](session-title.md) | 中文 -[`@deepseek-ai/dsh-session-title`](../../packages/session-title/session-title) 所拥有的持久、后写覆盖的标题状态与可选异步提供方词汇。共享 LLM(大语言模型)辅助组件负责精确的辅助请求记录。各包 README 负责时序、回退、失败与 fork 行为;生成的[持久化日志事件目录](../persistence-catalog.md)负责完整的事件声明。 +[`@deepseek-ai/dsh-session-title`](../../packages/session/session-title) 所拥有的持久、后写覆盖的标题状态与可选异步提供方词汇。共享 LLM(大语言模型)辅助组件负责精确的辅助请求记录。各包 README 负责时序、回退、失败与 fork 行为;生成的[持久化日志事件目录](../persistence-catalog.md)负责完整的事件声明。 -源码:[`packages/session-title/session-title/src/index.ts`](../../packages/session-title/session-title/src/index.ts)、[`packages/session-title/session-title-llm/src/index.ts`](../../packages/session-title/session-title-llm/src/index.ts) +源码:[`packages/session/session-title/src/index.ts`](../../packages/session/session-title/src/index.ts)、[`packages/session/session-title-llm/src/index.ts`](../../packages/session/session-title-llm/src/index.ts) ## 持久标题状态 @@ -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 new file mode 100644 index 0000000000..4e4802ccfa --- /dev/null +++ b/docs/subsystems/session.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.md +session.md: 3f018460df6d61b9146ac496bcac3a20e72430bc +session.zh.md: 2ce90264669bd664990fe91e242b2830a2398253 diff --git a/docs/core-data-structures/session.md b/docs/subsystems/session.md similarity index 76% rename from docs/core-data-structures/session.md rename to docs/subsystems/session.md index 5d54b03df8..3f018460df 100644 --- a/docs/core-data-structures/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:810`](../../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) + +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/core-data-structures/session.zh.md b/docs/subsystems/session.zh.md similarity index 76% rename from docs/core-data-structures/session.zh.md rename to docs/subsystems/session.zh.md index 5a867cfe30..2ce9026466 100644 --- a/docs/core-data-structures/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:810`](../../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) + +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/core-data-structures/goal.i18n.yaml b/docs/subsystems/settings.i18n.yaml similarity index 56% rename from docs/core-data-structures/goal.i18n.yaml rename to docs/subsystems/settings.i18n.yaml index 625556f99a..5c40419366 100644 --- a/docs/core-data-structures/goal.i18n.yaml +++ b/docs/subsystems/settings.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/settings.md +settings.md: 9256bf9436d2e77093fc8c6a3728b62fc4e8d67f +settings.zh.md: f6d662ebaf35eeb9d046353ea1087b0eb86e7068 diff --git a/docs/core-data-structures/settings.md b/docs/subsystems/settings.md similarity index 51% rename from docs/core-data-structures/settings.md rename to docs/subsystems/settings.md index bd01c1d284..9256bf9436 100644 --- a/docs/core-data-structures/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/core-data-structures/settings.zh.md b/docs/subsystems/settings.zh.md similarity index 50% rename from docs/core-data-structures/settings.zh.md rename to docs/subsystems/settings.zh.md index 1cb7f8b507..f6d662ebaf 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..84628f50b7 --- /dev/null +++ b/docs/subsystems/skills.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/skills.md +skills.md: 78d510fafca0017b9528a215270172a74e9b0de1 +skills.zh.md: e88c9f1ba190eb1e90966d35c7dcc2eb86ecd6cc diff --git a/docs/core-data-structures/skills.md b/docs/subsystems/skills.md similarity index 78% rename from docs/core-data-structures/skills.md rename to docs/subsystems/skills.md index d862cbc071..78d510fafc 100644 --- a/docs/core-data-structures/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/core-data-structures/skills.zh.md b/docs/subsystems/skills.zh.md similarity index 77% rename from docs/core-data-structures/skills.zh.md rename to docs/subsystems/skills.zh.md index 3f8c034ec2..e88c9f1ba1 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..67b938a93a --- /dev/null +++ b/docs/subsystems/spill.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/spill.md +spill.md: 41a3fdaf7d9b15fbc6d479724f77f579b8a98738 +spill.zh.md: 02a6fc4e88dfcfdb47f7639d988fc09c6bc42c69 diff --git a/docs/core-data-structures/spill.md b/docs/subsystems/spill.md similarity index 71% rename from docs/core-data-structures/spill.md rename to docs/subsystems/spill.md index a798d8143b..41a3fdaf7d 100644 --- a/docs/core-data-structures/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/core-data-structures/spill.zh.md b/docs/subsystems/spill.zh.md similarity index 71% rename from docs/core-data-structures/spill.zh.md rename to docs/subsystems/spill.zh.md index 1167c6f985..02a6fc4e88 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..98092af5b3 --- /dev/null +++ b/docs/subsystems/storage.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/storage.md +storage.md: 52dc84b699921e911276ce5214bc975abfd22fa4 +storage.zh.md: f614b77153e835264e1650515946a9d2acf70e57 diff --git a/docs/subsystems/storage.md b/docs/subsystems/storage.md new file mode 100644 index 0000000000..52dc84b699 --- /dev/null +++ b/docs/subsystems/storage.md @@ -0,0 +1,229 @@ +# Storage + +English | [中文](storage.zh.md) + +The storage subsystem persists everything that is not a session event log (session logs have their own seam — [persistence.md](persistence.md)). It is one optional capability, not part of the agent-loop spine, split as a [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): the hub and backend interface ([dsh-storage](../../packages/storage/storage), `ctx.storage`), the backend implementations ([dsh-storage-json](../../packages/storage/storage-json), registered as `json`, and [dsh-storage-sqlite](../../packages/storage/storage-sqlite), registered as `sqlite`), and the domain data form ([dsh-storage-domain](../../packages/storage/storage-domain), `ctx.storageDomain`, also reachable as `ctx.storage.domain`) — the backend seam's only consumer and the typed API everything else uses. The hub performs no IO itself: backends own media, data forms own semantics, and product packages never touch backends directly. Design record: [domain KV storage Agent Note](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md). + +Source: [`packages/storage/storage/src/backend.ts`](../../packages/storage/storage/src/backend.ts) · [`packages/storage/storage-domain/src/spec.ts`](../../packages/storage/storage-domain/src/spec.ts) · [`packages/storage/storage-domain/src/events.ts`](../../packages/storage/storage-domain/src/events.ts) + +## The hub: `ctx.storage` + +`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: + +```ts type-equiv +/** + * Data forms mountable on the hub, keyed by form name. Form owners extend + * this map via declaration merging (the domain layer merges + * `domain: DomainFacility`) and mount the facility in their `apply`. + */ +interface StorageForms {} +``` + +`mount(form, facility)` is an effect whose disposer unmounts; a second mount of the same key throws `duplicate-mount`. `form(form)` resolves a mounted facility and throws `form-not-mounted` until the owning plugin loads — assemblies order plugins accordingly rather than silently deferring. The domain layer merges `domain: DomainFacility`, so `ctx.storage.domain` and `ctx.storageDomain` are the same object. + +## The backend seam + +```ts type-equiv +/** + * One registered backend. A backend owns exactly one medium and shares its + * lifecycle across all facets; facets are optional members — a backend that + * cannot serve a shape simply omits it, and resolution fails loud instead. + */ +interface StorageBackend { + /** Key-value data shape; absent when this backend cannot serve it. */ + readonly kv?: KvFacet + + /** + * Drain in-flight writes across all open units and release the medium. + * Idempotent; concurrent and repeated calls resolve once teardown finishes. + * @returns resolution after the medium is released. + */ + close(): Promise +} +``` + +A backend owns one medium (a file-tree root, a database file) and exposes optional data-shape facets; `kv` is the only facet today. `KvFacet.open(descriptor)` opens one named unit — `KvUnitDescriptor` carries the name, format version, table names, and whether a global singleton slot exists — and returns a `KvUnit` with `loadAll`, `putRecord`, `deleteRecord`, `setGlobal`, and `close`. Unit and table names must match `UNIT_NAME_RE` (safe as a file name and as a SQL identifier segment); record keys are arbitrary strings that never reach file paths. A unit does not serialize concurrent writes — ordering belongs to the caller — but each single call is atomic on the medium and durable once resolved. A medium stamped with a different version rejects `version-mismatch`; one that cannot be parsed as the unit rejects `malformed-medium` (no migration, pre-release stance). [`backend.ts`](../../packages/storage/storage/src/backend.ts) is the normative clause-by-clause contract, and the shared conformance suite in [`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) asserts every clause against each backend. The [json backend](../../packages/storage/storage-json/README.md) republishes one whole human-readable file per unit atomically; the [sqlite backend](../../packages/storage/storage-sqlite/README.md) stores document-per-row in one database, the route for high-churn domains. + +## Declaring a domain + +A domain is declared once by its owning package as a spec object — the single source of the domain's identity, layout, and record schemas (zod, so `z.infer` keeps consumer types un-duplicated): + +```ts type-equiv +/** Static declaration of one domain: identity, version, and record layout. */ +interface DomainSpec { + /** Domain name; must match `UNIT_NAME_RE` (doubles as the backend unit name). */ + readonly name: string + /** Domain format version; a medium stamped with a different version rejects at open. */ + readonly version: number + /** Optional global singleton slot. */ + readonly global?: DomainGlobalSpec + /** Table declarations keyed by table name; each name must match `UNIT_NAME_RE`. */ + readonly tables: Record +} +``` + +`defineDomain(spec)` pins the spec's literal types and fails loud at the owner's module load, before any medium is touched: a domain or table name outside `UNIT_NAME_RE`, a version that is not a non-negative integer, or a global schema that accepts `null` all throw (`null` is the medium's "never written" sentinel, so a stored nullable global could not round-trip). `domainTable(schema)` declares one table with a phantom compile-time key type (typically a [branded id](core.md#branded-ids)); `descriptorOf(spec)` projects the backend-facing unit descriptor. + +## The open domain + +```ts type-equiv +/** One open domain, typed by its spec. */ +interface Domain { + /** Domain name from the spec. */ + readonly name: string + /** Global singleton handle; a spec without `global` has no usable handle (`never`). */ + readonly global: DomainGlobalHandleOf + /** + * Resolve one declared table handle. Handles are stable — repeated calls + * return the same instance. + * @param name - Declared table name. + * @returns the typed table handle. + */ + table(name: N): KvTable, TableValueOf> + + /** + * Close this domain: reject new writes immediately, drain already-queued + * writes (their events still emit), release the backend unit, then free + * the domain name for a later open. Idempotent — repeated calls share one + * teardown. The consumer owns this call (typically as its own `ctx.effect` + * disposer); the facility closes any domain left open when it unmounts. + * @returns resolution after the unit is released. + */ + close(): Promise +} +``` + +Reads are synchronous from authoritative in-memory state: `KvTable` exposes `get`/`entries`/`keys`/`size` (snapshot iterators that stay stable while queued writes land), and the global handle's `get()` serves the spec's `initial` until the first `set` materializes the slot on the medium. Every write — `put`, `delete`, `update`, `global.set` — queues on one per-domain chain and reaches backend durability first, then mutates memory, then emits `domain/changed`; a rejected backend write leaves memory untouched, so reads never diverge from the medium. `update(key, fn)` is an atomic read-modify-write at its chain slot (a missing key rejects `missing-key`); `delete` of an absent key resolves `false` with no write and no event. Returned records are the stored objects themselves, not copies — replace via `put`/`update`, never mutate in place. + +## The domain facility: `ctx.storageDomain` + +`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](#domainchanged--emit)): + +```ts type-equiv +/** Shared location fields of one durable domain change. */ +interface DomainChangedBase { + /** Owning domain name. */ + readonly domain: string + /** Table name; `''` for a global-singleton write. */ + readonly table: string + /** Record key; `''` for a global-singleton write. */ + readonly key: string +} +``` + +```ts type-equiv +/** One durable domain change; a closed union — switch on `operation`. */ +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 new file mode 100644 index 0000000000..f614b77153 --- /dev/null +++ b/docs/subsystems/storage.zh.md @@ -0,0 +1,229 @@ +# 存储 + +[English](storage.md) | 中文 + +存储子系统持久保存一切不属于会话事件日志的数据(会话日志有自己的 seam——见 [persistence.md](persistence.md))。它是一项可选能力,不属于 agent loop(智能体循环)主干,并按[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)拆分:枢纽(hub)与后端接口([dsh-storage](../../packages/storage/storage),`ctx.storage`)、后端实现(注册为 `json` 的 [dsh-storage-json](../../packages/storage/storage-json) 与注册为 `sqlite` 的 [dsh-storage-sqlite](../../packages/storage/storage-sqlite)),以及领域数据形式([dsh-storage-domain](../../packages/storage/storage-domain),`ctx.storageDomain`,也可经 `ctx.storage.domain` 访问)——它是后端 seam 的唯一消费方,也是其他一切所使用的类型化 API。枢纽自身不做任何 IO:后端拥有介质,数据形式拥有语义,产品包(package)绝不直接触碰后端。设计记录:[领域 KV 存储 Agent Note(agent 决策记录)](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md)。 + +源码:[`packages/storage/storage/src/backend.ts`](../../packages/storage/storage/src/backend.ts) · [`packages/storage/storage-domain/src/spec.ts`](../../packages/storage/storage-domain/src/spec.ts) · [`packages/storage/storage-domain/src/events.ts`](../../packages/storage/storage-domain/src/events.ts) + +## 枢纽:`ctx.storage` + +`Storage`([签名](#ctxstorage--storage))是汇合点,不是存储本体。`ctx.storage.backend` 是一张名称 → 后端的表:多个后端并排保持挂载,哪个后端服务哪个消费方由该消费方自己的配置决定(即领域层的路由表),绝不是枢纽全局的选择。`register(name, backend)` 返回 disposer;重复名称与查找未知名称都抛出 `StorageError`。dispose(资源释放)只注销名称——由拥有插件在注销之后自行关闭后端。每个后端插件还会发布一个仅用于生命周期的服务键(`storageBackendServiceKey(name)`),数据形式提供方注入它,使自身激活不会与后端注册发生竞态。 + +数据形式以一张可合并扩展的键 map 挂载到枢纽上: + +```ts type-equiv +/** + * Data forms mountable on the hub, keyed by form name. Form owners extend + * this map via declaration merging (the domain layer merges + * `domain: DomainFacility`) and mount the facility in their `apply`. + */ +interface StorageForms {} +``` + +`mount(form, facility)` 是一个 effect,其 disposer 负责卸载;对同一键的第二次挂载抛出 `duplicate-mount`。`form(form)` 解析已挂载的 facility,在拥有插件加载之前抛出 `form-not-mounted`——组合方应据此安排插件顺序,而不是静默推迟。领域层合并 `domain: DomainFacility`,因此 `ctx.storage.domain` 与 `ctx.storageDomain` 是同一个对象。 + +## 后端 seam + +```ts type-equiv +/** + * One registered backend. A backend owns exactly one medium and shares its + * lifecycle across all facets; facets are optional members — a backend that + * cannot serve a shape simply omits it, and resolution fails loud instead. + */ +interface StorageBackend { + /** Key-value data shape; absent when this backend cannot serve it. */ + readonly kv?: KvFacet + + /** + * Drain in-flight writes across all open units and release the medium. + * Idempotent; concurrent and repeated calls resolve once teardown finishes. + * @returns resolution after the medium is released. + */ + close(): Promise +} +``` + +一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并暴露可选的数据形状 facet;目前 `kv` 是唯一的 facet。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、格式版本、表名清单,以及是否存在全局单例槽位——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键是任意字符串,绝不进入文件路径。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的,且 resolve 后即已持久。介质上记录的版本与之不同时拒绝 `version-mismatch`;无法按该 unit 解析的介质拒绝 `malformed-medium`(不做迁移:预发布立场)。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性契约,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件对每个后端断言其中每一条款。[json 后端](../../packages/storage/storage-json/README.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.md)在单个数据库中按一行一文档存储,是高频更新领域的路由选择。 + +## 声明领域 + +领域由其拥有包声明一次,形式是一个 spec 对象——它是该领域的身份、布局和记录 schema 的单一来源(schema 用 zod 编写,因此 `z.infer` 让消费方类型无需重复声明): + +```ts type-equiv +/** Static declaration of one domain: identity, version, and record layout. */ +interface DomainSpec { + /** Domain name; must match `UNIT_NAME_RE` (doubles as the backend unit name). */ + readonly name: string + /** Domain format version; a medium stamped with a different version rejects at open. */ + readonly version: number + /** Optional global singleton slot. */ + readonly global?: DomainGlobalSpec + /** Table declarations keyed by table name; each name must match `UNIT_NAME_RE`. */ + readonly tables: Record +} +``` + +`defineDomain(spec)` 固定 spec 的字面量类型,并在拥有方的模块加载时、任何介质被触碰之前就大声失败:领域名或表名不匹配 `UNIT_NAME_RE`、版本不是非负整数、global schema 接受 `null`,这些都会抛出(`null` 是介质的「从未写入」哨兵值,可空的 global 一旦存储就无法往返还原)。`domainTable(schema)` 声明一张表,其键类型是仅存在于编译期的 phantom 类型(通常是[品牌化 id](core.md#branded-ids));`descriptorOf(spec)` 投影出面向后端的 unit 描述符。 + +## 打开的领域 + +```ts type-equiv +/** One open domain, typed by its spec. */ +interface Domain { + /** Domain name from the spec. */ + readonly name: string + /** Global singleton handle; a spec without `global` has no usable handle (`never`). */ + readonly global: DomainGlobalHandleOf + /** + * Resolve one declared table handle. Handles are stable — repeated calls + * return the same instance. + * @param name - Declared table name. + * @returns the typed table handle. + */ + table(name: N): KvTable, TableValueOf> + + /** + * Close this domain: reject new writes immediately, drain already-queued + * writes (their events still emit), release the backend unit, then free + * the domain name for a later open. Idempotent — repeated calls share one + * teardown. The consumer owns this call (typically as its own `ctx.effect` + * disposer); the facility closes any domain left open when it unmounts. + * @returns resolution after the unit is released. + */ + close(): Promise +} +``` + +读取是同步的,来自权威的内存态:`KvTable` 暴露 `get`/`entries`/`keys`/`size`(快照迭代器,在排队写入落地期间保持稳定),global 句柄的 `get()` 在第一次 `set` 将槽位物化到介质之前一直返回 spec 的 `initial`。每次写入——`put`、`delete`、`update`、`global.set`——都在同一条逐领域写链上排队,先到达后端持久性,再更新内存,最后发出 `domain/changed`;后端写入被拒时内存原样不动,因此读取绝不会偏离介质。`update(key, fn)` 在其写链槽位上是一次原子的读-改-写(键缺失时拒绝 `missing-key`);`delete` 一个不存在的键 resolve 为 `false`,不产生写入也不产生事件。返回的记录就是存储的对象本身,不是副本——请经 `put`/`update` 整体替换,绝不要就地修改。 + +## 领域 facility:`ctx.storageDomain` + +`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` + +每次持久写入都发出一个事件,严格发生在后端确认持久性之后,顺序遵循该领域的写链([事件条目](#domainchanged--emit)): + +```ts type-equiv +/** Shared location fields of one durable domain change. */ +interface DomainChangedBase { + /** Owning domain name. */ + readonly domain: string + /** Table name; `''` for a global-singleton write. */ + readonly table: string + /** Record key; `''` for a global-singleton write. */ + readonly key: string +} +``` + +```ts type-equiv +/** One durable domain change; a closed union — switch on `operation`. */ +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 new file mode 100644 index 0000000000..994c04eaa5 --- /dev/null +++ b/docs/subsystems/subagent.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/subagent.md +subagent.md: 3bf3ac7c48bfa08be86314bb43263d4ee6198654 +subagent.zh.md: ddc3a715fbb6589dffc38f0e0c8b23bf35dddf6e diff --git a/docs/core-data-structures/subagent.md b/docs/subsystems/subagent.md similarity index 73% rename from docs/core-data-structures/subagent.md rename to docs/subsystems/subagent.md index b26a12d1d5..3bf3ac7c48 100644 --- a/docs/core-data-structures/subagent.md +++ b/docs/subsystems/subagent.md @@ -133,7 +133,7 @@ persisted Session | `waiting` | wake the same Activation | | no Activation | cold-resume a new Activation | -`running` means the Agent has an active admission or turn, or waking inbox work; `waiting` means it is quiescent but still owns at least one child Activation that has not completed disposal; `settled` means quiescent with every owned child disposed, at which point the manager disposes the `AgentHandle` and removes the Activation. The manager derives these internal conditions from Agent quiescence and the owned-child set rather than maintaining a second execution state machine. +`running` means the Agent has an active admission or turn, or waking inbox work; `waiting` means it is quiescent but still owns at least one child Activation that has not completed disposal; `settled` means quiescent with every owned child disposed, at which point the manager disposes the [`AgentHandle`](core.md#creation-and-ownership) and removes the Activation. The manager derives these internal conditions from Agent quiescence and the owned-child set rather than maintaining a second execution state machine. The Agent inbox is the only queue. Every continuation message becomes one `Agent.followup()` FIFO turn, so accepted messages have one observable order and a follow-up cannot redirect a turn already underway. Successful delivery returns the accepted `MessageId`; the existing `agent/inbox/enqueue`, `agent/inbox/dequeue`, and `agent/inbox/discard` events remain the message-lifecycle observations, and the continuation layer defines no subagent-specific delivery route. @@ -432,4 +432,270 @@ Provider `start()` fulfills with a published run. The service mints a unique `ru The spawn and fork backends create an ordinary one-shot agent through `parent.ctx`, pass cancellation into core creation, and dispose through `AgentHandle`; a continuable child is instead created by the continuation manager through its own activation-owner scope. Provider removal blocks new starts without revoking accepted runs. Each child gets a new flat scope rather than inheriting parent registrations. Depth and fork seeding reuse existing agent and session vocabulary: - **Delegation depth** is durable `SessionHeader.delegationDepth` plus the merge-extensible runtime field `AgentOptions.subagentDepth`; absence means top-level depth zero, and the greater present value is authoritative. The seam owns both fields — the loop neither sets nor reads them — so an in-process child persists parent depth + 1, cold resume cannot lower it, and every start rejects a derived depth outside the safe-integer domain or above a defined absolute `request.maxDepth` cap. -- **Fork seeding** uses `CreateAgentOptions.seed` (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `ctx.agents.resume()` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and the [invariants](../../packages/support/invariants) replay accepts it (the in-flight, unbalanced turn is excluded). +- **Fork seeding** uses [`CreateAgentOptions.seed`](core.md#creation-and-ownership) (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `ctx.agents.resume()` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and the [invariants](../../packages/support/invariants) replay accepts it (the in-flight, unbalanced turn is excluded). + + + + + +## 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](llm-streaming.md) · [MessageId](llm-streaming.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/core-data-structures/subagent.zh.md b/docs/subsystems/subagent.zh.md similarity index 73% rename from docs/core-data-structures/subagent.zh.md rename to docs/subsystems/subagent.zh.md index 6c4c64ff22..ddc3a715fb 100644 --- a/docs/core-data-structures/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -133,7 +133,7 @@ persisted Session | `waiting` | 唤醒同一 Activation | | 无 Activation | 冷恢复一个新的 Activation | -`running` 表示 Agent 拥有活跃的准入或轮次,或正在唤醒收件箱工作;`waiting` 表示它已停稳,但仍拥有至少一个尚未完成 dispose 的子 Activation;`settled` 表示已停稳且其拥有的每个子级都已 dispose,此时管理器会 dispose `AgentHandle` 并移除该 Activation。管理器根据 Agent 的完全停稳状态与其拥有的子级集合推导这些内部条件,而非维护第二套执行状态机。 +`running` 表示 Agent 拥有活跃的准入或轮次,或正在唤醒收件箱工作;`waiting` 表示它已停稳,但仍拥有至少一个尚未完成 dispose 的子 Activation;`settled` 表示已停稳且其拥有的每个子级都已 dispose,此时管理器会 dispose [`AgentHandle`](core.md#creation-and-ownership) 并移除该 Activation。管理器根据 Agent 的完全停稳状态与其拥有的子级集合推导这些内部条件,而非维护第二套执行状态机。 Agent 收件箱是唯一的队列。每条继续执行消息都会成为一个 `Agent.followup()` FIFO 轮次,因此已接受的消息共享同一个可观测顺序,且后续消息无法改变已在进行中的轮次。投递成功会返回被接受的 `MessageId`;既有的 `agent/inbox/enqueue`、`agent/inbox/dequeue` 与 `agent/inbox/discard` 事件仍是消息生命周期的观测点,继续执行层不定义任何 subagent 专属的投递路由。 @@ -434,4 +434,270 @@ interface SubagentProvider { spawn 和 fork 后端通过 `parent.ctx` 创建一个普通的单次 agent,将取消信号传入核心创建流程,并通过 `AgentHandle` 进行 dispose;而可继续子 agent 则由继续执行管理器通过其自己的 activation-owner 作用域创建。移除提供方会阻止新的 start,但不会撤销已接受的 run。每个子 agent 获得一个新的扁平作用域,而非继承父级注册。深度与 fork 种子注入复用既有的 agent 和会话词汇: - **委派深度**由持久 `SessionHeader.delegationDepth` 与可合并扩展的运行时字段 `AgentOptions.subagentDepth` 共同表示;缺失表示顶层深度为零,存在的较大值具有权威性。两个字段都归该 seam 所有——循环既不设置也不读取它们——因此进程内子 agent 会持久保存 parent 深度 + 1,冷恢复无法降低深度,而且每次 start 都会拒绝超出安全整数域、或高于已定义绝对 `request.maxDepth` 上限的派生深度。 -- **Fork 种子注入**使用 `CreateAgentOptions.seed`(一个 `SessionEvent[]` 前缀,经由 `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })` 传递,与 `ctx.agents.resume()` 使用的原语相同)。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*——父级事件直到并包括其最后一个 `turn/end`——因此种子从 0 连续,[invariants](../../packages/support/invariants) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。 +- **Fork 种子注入**使用 [`CreateAgentOptions.seed`](core.md#creation-and-ownership)(一个 `SessionEvent[]` 前缀,经由 `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })` 传递,与 `ctx.agents.resume()` 使用的原语相同)。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*——父级事件直到并包括其最后一个 `turn/end`——因此种子从 0 连续,[invariants](../../packages/support/invariants) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。 + + + + + +## 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](llm-streaming.md) · [MessageId](llm-streaming.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 new file mode 100644 index 0000000000..a3071e98a9 --- /dev/null +++ b/docs/subsystems/subprocess.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/subprocess.md +subprocess.md: 0bc2bf86518bcc60bef02554b94471425f2ac433 +subprocess.zh.md: 5d6425707edcf1952c25dbdc91dbf7aab3779350 diff --git a/docs/core-data-structures/subprocess.md b/docs/subsystems/subprocess.md similarity index 73% rename from docs/core-data-structures/subprocess.md rename to docs/subsystems/subprocess.md index 023b122218..0bc2bf8651 100644 --- a/docs/core-data-structures/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/core-data-structures/subprocess.zh.md b/docs/subsystems/subprocess.zh.md similarity index 73% rename from docs/core-data-structures/subprocess.zh.md rename to docs/subsystems/subprocess.zh.md index 5ee9c782c0..5d6425707e 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..60e354491e --- /dev/null +++ b/docs/subsystems/system-prompt.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/system-prompt.md +system-prompt.md: 5397858ea9991efad06e045118b96a90386f2285 +system-prompt.zh.md: 9e70779711c039704c076cc8dca350c91695add2 diff --git a/docs/subsystems/system-prompt.md b/docs/subsystems/system-prompt.md new file mode 100644 index 0000000000..5397858ea9 --- /dev/null +++ b/docs/subsystems/system-prompt.md @@ -0,0 +1,188 @@ +# System Prompt Assembly + +English | [中文](system-prompt.zh.md) + +The [system-prompt package](../../packages/core/system-prompt) owns the data exchanged between prompt contributors and one assembly call. The package [README](../../packages/core/system-prompt/README.md) documents registration, ordering, scoping, and rendering behavior; this page pins the literal cross-package shapes that plugins implement or pass. + +Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts). + +## Assembly context + +`AssembleContext` identifies the scope layer one assembly resolves and may carry the explicit control signal for that request. It is merge-extensible: `dsh-agent` adds the optional live `agent` field, and `assembleContextFor(agent, signal)` sets the explicit fields together. A bare assembly has neither scope nor signal. + +```ts type-equiv +/** Merge-extensible context for one prompt assembly. */ +interface AssembleContext { + /** + * Scope whose providers and waterfall listeners participate. When absent, + * only global providers and subject-less listeners participate. + */ + scope?: ScopeKey + /** Explicit control signal for the turn that requested this assembly, when any. */ + signal?: AbortSignal +} +``` + +## Tool-provider result + +`ToolProviderResult.schemas` is the model-visible set for the current assembly. `knownNames` is the provider's pre-restriction name universe used to distinguish a configured-name typo from a known tool that is deliberately hidden in this scope. + +```ts type-equiv +/** Tool schemas visible in one assembly and their pre-restriction name set. */ +interface ToolProviderResult { + /** The schemas this provider contributes to THIS assembly. */ + readonly schemas: readonly ToolSchema[] + /** The pre-restriction name universe for config validation (defaults to `schemas`' names). */ + readonly knownNames?: readonly string[] +} +``` + +## Prompt sections + +`PromptSection` is a readonly same-process registration contract. Its text may be static or resolved from the current assembly context. + +```ts type-equiv +/** One contributed section of the system prompt (registry input). */ +interface PromptSection { + /** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */ + readonly name: string + /** + * Sections are concatenated in ascending order. Convention: `-100` is the + * harness identity, `0` the deployment persona, tool guidance uses 100–199; + * other negative orders also render before the persona. + */ + readonly order: number + /** + * Static text or a provider evaluated at each assembly with that assembly's + * {@link AssembleContext}. The text may reference `{{variable}}`s — they are + * interpolated later, by {@link renderPrompt}. + */ + readonly text: string | ((context: AssembleContext) => string) +} +``` + +## Dynamic prompt context + +`PromptContext` is the cache-safe counterpart to `PromptSection`. The assembly resolves and orders these contributions, while agent-loop logs their complete current snapshot after retained model history only when it changed or compaction removed it. + +```ts type-equiv +/** Dynamic model context materialized as a durable user-role snapshot. */ +interface PromptContext { + /** Unique name — a duplicate registration throws (see {@link SystemPrompt.context}). */ + readonly name: string + /** Contexts are joined in ascending order. */ + readonly order: number + /** Static text or a provider evaluated for each assembly. Empty text contributes nothing. */ + 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:325`](../../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 new file mode 100644 index 0000000000..9e70779711 --- /dev/null +++ b/docs/subsystems/system-prompt.zh.md @@ -0,0 +1,188 @@ +# 系统提示词组装 + +[English](system-prompt.md) | 中文 + +[system-prompt 包](../../packages/core/system-prompt)负责管理提示词贡献者与一次组装调用之间交换的数据。该包的 [README](../../packages/core/system-prompt/README.md) 记录了注册、排序、作用域与渲染行为;本页固定各插件实现或传递的跨包字面形状。 + +源码:[`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)。 + +## 组装上下文 + +`AssembleContext` 标识一次组装所解析的作用域层,并可携带该请求的显式控制信号。它可合并扩展:`dsh-agent` 添加可选字段 `agent`,用于携带当前的 agent(智能体)实例;`assembleContextFor(agent, signal)` 则一起设置这些显式字段。裸组装既没有作用域,也没有信号。 + +```ts type-equiv +/** Merge-extensible context for one prompt assembly. */ +interface AssembleContext { + /** + * Scope whose providers and waterfall listeners participate. When absent, + * only global providers and subject-less listeners participate. + */ + scope?: ScopeKey + /** Explicit control signal for the turn that requested this assembly, when any. */ + signal?: AbortSignal +} +``` + +## 工具提供方结果 + +`ToolProviderResult.schemas` 是当前组装中对模型可见的工具 schema 集合。`knownNames` 是提供方在限制前的名称全集,用于区分「配置名拼写错误」与「已知工具在此作用域中被有意隐藏」。 + +```ts type-equiv +/** Tool schemas visible in one assembly and their pre-restriction name set. */ +interface ToolProviderResult { + /** The schemas this provider contributes to THIS assembly. */ + readonly schemas: readonly ToolSchema[] + /** The pre-restriction name universe for config validation (defaults to `schemas`' names). */ + readonly knownNames?: readonly string[] +} +``` + +## 提示词段落 + +`PromptSection` 是一份只读的同进程注册契约。其文本可以是静态的,也可以从当前组装上下文动态解析。 + +```ts type-equiv +/** One contributed section of the system prompt (registry input). */ +interface PromptSection { + /** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */ + readonly name: string + /** + * Sections are concatenated in ascending order. Convention: `-100` is the + * harness identity, `0` the deployment persona, tool guidance uses 100–199; + * other negative orders also render before the persona. + */ + readonly order: number + /** + * Static text or a provider evaluated at each assembly with that assembly's + * {@link AssembleContext}. The text may reference `{{variable}}`s — they are + * interpolated later, by {@link renderPrompt}. + */ + readonly text: string | ((context: AssembleContext) => string) +} +``` + +## 动态提示词上下文 + +`PromptContext` 是与 `PromptSection` 对应的缓存安全结构。组装会解析这些贡献并排序;agent loop(智能体循环)仅在完整当前快照发生变化或被压缩(compaction)移除时,才会将其记录在保留的模型历史之后。 + +```ts type-equiv +/** Dynamic model context materialized as a durable user-role snapshot. */ +interface PromptContext { + /** Unique name — a duplicate registration throws (see {@link SystemPrompt.context}). */ + readonly name: string + /** Contexts are joined in ascending order. */ + readonly order: number + /** Static text or a provider evaluated for each assembly. Empty text contributes nothing. */ + 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:325`](../../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 new file mode 100644 index 0000000000..d28dcc2781 --- /dev/null +++ b/docs/subsystems/tasks.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/tasks.md +tasks.md: 6237607307cbab8511f968ed591e3e3b8b845c6a +tasks.zh.md: 99a6fcc4f54c1b52e5c6fd393327cc768f064297 diff --git a/docs/core-data-structures/tasks.md b/docs/subsystems/tasks.md similarity index 57% rename from docs/core-data-structures/tasks.md rename to docs/subsystems/tasks.md index a38055d3ef..6237607307 100644 --- a/docs/core-data-structures/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/core-data-structures/tasks.zh.md b/docs/subsystems/tasks.zh.md similarity index 56% rename from docs/core-data-structures/tasks.zh.md rename to docs/subsystems/tasks.zh.md index f34d42e713..99a6fcc4f5 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..19cd6f6851 --- /dev/null +++ b/docs/subsystems/telemetry.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/telemetry.md +telemetry.md: 131c5814f29d70c356989e62fda1db5c1c5448d6 +telemetry.zh.md: 5d4a9d9994457bc06a9e4194c4807f88789a3cf0 diff --git a/docs/subsystems/telemetry.md b/docs/subsystems/telemetry.md new file mode 100644 index 0000000000..131c5814f2 --- /dev/null +++ b/docs/subsystems/telemetry.md @@ -0,0 +1,180 @@ +# Telemetry + +English | [中文](telemetry.zh.md) + +Outbound session reporting, split as a [capability seam](../capability-seams.md): the seam ([dsh-session-telemetry](../../packages/session/session-telemetry), `ctx.telemetry`) owns the capture points, the fixed chunk projection, the `telemetry/record` redaction waterfall, the handoff cursor, and the minimal backend contract; the backend a deployment loads ([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel)) is the OpenTelemetry JS SDK's log pipeline configured verbatim. It is one optional capability, not part of the agent-loop spine, and nothing here reaches a model request. The boundary axiom — the harness's aspect ends at `emit()`; batching, retry, queueing, and loss policy belong to the reporting SDK — and the rejected alternatives are pinned in the [revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md); the capture points, cursor, and projection contracts live in the [seam README](../../packages/session/session-telemetry/README.md). + +Source: [`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts) + +## The logical record + +```ts type-equiv +/** + * Severity of a telemetry record, pre-mapped at capture so a receiver can + * alert with zero configuration: `error` for events whose own outcome flag + * says so (the tool-result block's `isError`, `turn/end` error reasons) and for + * `agent-error` operational records. Captured events otherwise default to + * `info`; `warn` remains available to `telemetry/record` policies and + * backends. + */ +type TelemetrySeverity = 'info' | 'warn' | 'error' +``` + +```ts type-equiv +/** + * One logical record handed to a backend — the seam's whole outbound + * vocabulary. Ledger records mirror session-log events one-to-one; + * operational records (`channel: 'ops'`) carry the two signals with no log + * home (`agent-error`, `shutdown`) and deliberately omit `event.seq`-style + * identity so they can never be mistaken for ledger rows. + */ +interface TelemetryRecord { + /** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */ + channel: 'ledger' | 'ops' + /** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */ + time: number + /** Pre-mapped alerting severity; see {@link TelemetrySeverity}. */ + severity: TelemetrySeverity + /** + * Identity attributes, deliberately minimal: ledger records carry + * `session.id`, `event.type`, `event.seq`, plus `session.cwd` / + * `session.parent_id` / `session.seed_length` when the header has them; + * ops records carry `telemetry.op`, `session.id`, and (for `agent-error`) + * `agent.id`, `turn`, `step`, `error.name`. Anything recoverable from the + * body is intentionally NOT duplicated here. + */ + attributes: Record + /** + * The complete payload: a deep copy of the session event's `data` for + * ledger records (JSON-serializable by `Session.append`'s own + * validation), or the op payload for ops records. Never mutated after + * handoff. + */ + body: unknown +} +``` + +Only the first `assistant/chunk` of each `(turn, step)` ships — the stream-started signal; the rest drop at capture, so `seq` gaps are routine on the wire and never a loss signal. Every other [session event](session.md) type, including plugin-merged ones the seam never heard of, passes through whole. Delivery is best-effort: the cursor marks handed-off, not delivered, records can be lost (crash, reload window) and duplicated (cursor-less re-adoption, SDK retries), so receivers dedupe ledger records on `(session.id, event.seq)`; ops records deliberately omit that identity — they are signals to alert on, not entries to sum, and tolerate duplicates instead. + +## The backend contract + +```ts type-equiv +/** + * The backend contract the coordinator hands records to — the minimum any + * reporting SDK satisfies with zero bending. {@link Telemetry} is its + * service-registered form; tests compose the coordinator with a bare + * implementation of this interface. + */ +interface TelemetryBackend { + /** + * Hand one record to the backend's pipeline. MUST be a non-blocking + * enqueue — the coordinator calls this synchronously from the + * `session/event` hot path or an explicit canonical-log capture, so anything + * slower than a queue push would tax the agent loop or feedback handling. + * Errors thrown here are contained by the coordinator and logged; they + * never reach the loop. + * @param record - the logical record to report; owned by the backend after the call. + */ + emit(record: TelemetryRecord): void + /** + * Optional hint that a natural boundary (turn end) passed — a backend may + * forward it to its SDK's flush so records land at turn boundaries. Called + * fire-and-forget; implementations must not block and must not throw + * meaningfully (the coordinator contains exceptions). Most backends should + * leave this unimplemented and let their SDK's own batching cadence govern + * export timing: a backend that does implement it owns the interaction + * between its concurrent flushes and {@link shutdown}'s drain (the OTel + * backend removed its implementation for exactly that hazard — see the + * revival Agent Note). + */ + flush?(): void + /** + * Forward the fiber's disposal to the SDK: flush whatever is queued and + * reach quiescence, per the SDK's own shutdown contract. Everything + * emitted before this call must still be delivered — including records + * enqueued while a {@link flush} hint is in flight, so a backend whose SDK + * guards against concurrent flushes orders behind the outstanding one (the + * coordinator emits its dispose-time `shutdown` markers immediately before + * calling this). Awaited by the coordinator's dispose; a rejection is + * logged as a warning and never fails application teardown. + * The coordinator captures dispose-time shutdown markers immediately before + * this call for live capture; on-demand capture creates no ops records. + * @returns resolves when the backend's pipeline has quiesced. + */ + shutdown(): Promise +} +``` + +`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](#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 new file mode 100644 index 0000000000..5d4a9d9994 --- /dev/null +++ b/docs/subsystems/telemetry.zh.md @@ -0,0 +1,180 @@ +# 遥测(telemetry) + +[English](telemetry.md) | 中文 + +对外的会话上报,拆分为一项[能力 seam](../capability-seams.md):seam 一侧([dsh-session-telemetry](../../packages/session/session-telemetry),`ctx.telemetry`)拥有捕获点、固定分片投影、`telemetry/record` 脱敏 waterfall(瀑布式事件)、handoff 游标与最小后端契约;部署方加载的后端([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel))则是原样配置的 OpenTelemetry JS SDK 日志流水线。它是一项可选能力,不属于 agent loop(智能体循环)主干,这里也没有任何内容会进入模型请求。边界公理(harness 的职责止于 `emit()`;批处理、重试、排队与丢失策略都属于上报 SDK)连同被否决的替代方案,均已在[复活 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)中定案;捕获点、游标与投影的契约见 [seam README](../../packages/session/session-telemetry/README.md)。 + +源码:[`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts) + +## 逻辑记录 + +```ts type-equiv +/** + * Severity of a telemetry record, pre-mapped at capture so a receiver can + * alert with zero configuration: `error` for events whose own outcome flag + * says so (the tool-result block's `isError`, `turn/end` error reasons) and for + * `agent-error` operational records. Captured events otherwise default to + * `info`; `warn` remains available to `telemetry/record` policies and + * backends. + */ +type TelemetrySeverity = 'info' | 'warn' | 'error' +``` + +```ts type-equiv +/** + * One logical record handed to a backend — the seam's whole outbound + * vocabulary. Ledger records mirror session-log events one-to-one; + * operational records (`channel: 'ops'`) carry the two signals with no log + * home (`agent-error`, `shutdown`) and deliberately omit `event.seq`-style + * identity so they can never be mistaken for ledger rows. + */ +interface TelemetryRecord { + /** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */ + channel: 'ledger' | 'ops' + /** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */ + time: number + /** Pre-mapped alerting severity; see {@link TelemetrySeverity}. */ + severity: TelemetrySeverity + /** + * Identity attributes, deliberately minimal: ledger records carry + * `session.id`, `event.type`, `event.seq`, plus `session.cwd` / + * `session.parent_id` / `session.seed_length` when the header has them; + * ops records carry `telemetry.op`, `session.id`, and (for `agent-error`) + * `agent.id`, `turn`, `step`, `error.name`. Anything recoverable from the + * body is intentionally NOT duplicated here. + */ + attributes: Record + /** + * The complete payload: a deep copy of the session event's `data` for + * ledger records (JSON-serializable by `Session.append`'s own + * validation), or the op payload for ops records. Never mutated after + * handoff. + */ + body: unknown +} +``` + +每个 `(turn, step)` 只发出第一条 `assistant/chunk`,即「流已开始」的信号;其余分片在捕获时丢弃,因此导出流中的 `seq` 缺口是常态,绝不是丢失信号。其他所有[会话事件](session.md)类型都会完整透传,包括该 seam 从未听说过、由插件合并进来的事件类型。投递是尽力而为的:游标标记的是「已交接」而非「已送达」,记录可能丢失(崩溃、重载窗口)也可能重复(无游标的重新接管、SDK 重试),因此接收端对 ledger 记录基于 `(session.id, event.seq)` 去重;ops 记录刻意省略这类标识——它们是用于告警的信号,而非用于累加的条目,重复被容忍而非被去重。 + +## 后端契约 + +```ts type-equiv +/** + * The backend contract the coordinator hands records to — the minimum any + * reporting SDK satisfies with zero bending. {@link Telemetry} is its + * service-registered form; tests compose the coordinator with a bare + * implementation of this interface. + */ +interface TelemetryBackend { + /** + * Hand one record to the backend's pipeline. MUST be a non-blocking + * enqueue — the coordinator calls this synchronously from the + * `session/event` hot path or an explicit canonical-log capture, so anything + * slower than a queue push would tax the agent loop or feedback handling. + * Errors thrown here are contained by the coordinator and logged; they + * never reach the loop. + * @param record - the logical record to report; owned by the backend after the call. + */ + emit(record: TelemetryRecord): void + /** + * Optional hint that a natural boundary (turn end) passed — a backend may + * forward it to its SDK's flush so records land at turn boundaries. Called + * fire-and-forget; implementations must not block and must not throw + * meaningfully (the coordinator contains exceptions). Most backends should + * leave this unimplemented and let their SDK's own batching cadence govern + * export timing: a backend that does implement it owns the interaction + * between its concurrent flushes and {@link shutdown}'s drain (the OTel + * backend removed its implementation for exactly that hazard — see the + * revival Agent Note). + */ + flush?(): void + /** + * Forward the fiber's disposal to the SDK: flush whatever is queued and + * reach quiescence, per the SDK's own shutdown contract. Everything + * emitted before this call must still be delivered — including records + * enqueued while a {@link flush} hint is in flight, so a backend whose SDK + * guards against concurrent flushes orders behind the outstanding one (the + * coordinator emits its dispose-time `shutdown` markers immediately before + * calling this). Awaited by the coordinator's dispose; a rejection is + * logged as a warning and never fails application teardown. + * The coordinator captures dispose-time shutdown markers immediately before + * this call for live capture; on-demand capture creates no ops records. + * @returns resolves when the backend's pipeline has quiesced. + */ + shutdown(): Promise +} +``` + +`Telemetry`(`ctx.telemetry`,[签名](#ctxtelemetry--telemetry-abstract-seam))是该契约的可加载形态:每个上下文只允许一个实现,重复加载会抛出异常;后端在其构造函数中组合 seam 的 `TelemetryCoordinator`,以此装配捕获侧。 + +## 脱敏 waterfall:`telemetry/record` + +每条记录在投影与 `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 new file mode 100644 index 0000000000..17bbb937cc --- /dev/null +++ b/docs/subsystems/token-meter.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/token-meter.md +token-meter.md: 8a01980191bf29b1fbc0f8b1d0b33d1fb149319c +token-meter.zh.md: 7411ce26cb75697607a706469d7923d46ff3f421 diff --git a/docs/core-data-structures/token-meter.md b/docs/subsystems/token-meter.md similarity index 50% rename from docs/core-data-structures/token-meter.md rename to docs/subsystems/token-meter.md index 05784e2944..8a01980191 100644 --- a/docs/core-data-structures/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](llm-streaming.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 new file mode 100644 index 0000000000..7411ce26cb --- /dev/null +++ b/docs/subsystems/token-meter.zh.md @@ -0,0 +1,90 @@ +# Token 计量 + +[English](token-meter.md) | 中文 + +`@deepseek-ai/dsh-token-meter` 公开一个独立的回放快照,用于表示请求压力与按位置计算的表层定价。`logRevision` 表示生成该计量中每个字段时所消费的持久事件数量。 + +来源:[`packages/llm/token-meter/src/types.ts`](../../packages/llm/token-meter/src/types.ts) + +## `TokenMeasurement` + +```ts type-equiv +/** Detached immutable request-pressure and surface snapshot at one consumed log revision. */ +interface TokenMeasurement { + /** Number of durable events consumed; equal to the next unread event seq. */ + readonly logRevision: number + /** Provider or heuristic anchor used for this measurement. */ + readonly baseline: TokenMeasurementBaseline + /** Signed repricing of current surface content relative to the baseline anchor. */ + readonly surfaceDeltaTokens: number + /** Non-negative current request-and-response pressure. */ + readonly totalTokens: number + /** Total heuristic tokens across the current surface. */ + readonly surfaceTokens: number + /** Current surface nodes in positional head-to-tail order. */ + readonly nodes: readonly TokenSurfaceNode[] +} +``` + +`baseline.kind === 'usage'` 表示最近一次成功的提供方调用具有相同的规范请求信封,且该调用的总量不低于其完整启发式锚点。`estimated` 表示不存在可复用的保守 usage 锚点,因此服务使用固定启发式规则对完整信封和表层定价。后续成功请求会替换早先的锚点;有符号的 `surfaceDeltaTokens` 会保留相对于匹配锚点的增长与缩减。`totalTokens` 仍表示请求与响应压力,`surfaceTokens` 则是仅针对表层的启发式总量,等于所有节点价格之和。 + +## `TokenSurfaceNode` + +```ts type-equiv +/** One token-priced node in the current ordered session surface. */ +interface TokenSurfaceNode { + /** Durable sequence number of the surface event. */ + readonly seq: number + /** Heuristic tokens for the exact message projected by this node. */ + readonly tokens: number +} +``` + +表层顺序具有权威性;替换节点的持久 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](llm-streaming.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 new file mode 100644 index 0000000000..21003bb796 --- /dev/null +++ b/docs/subsystems/tools.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/tools.md +tools.md: 0551ee1c9dfa12b415c1f4575b5225f771cb53bc +tools.zh.md: 50843b07f1d98b1c9d9222288b1273b34bee1e4a diff --git a/docs/core-data-structures/tools.md b/docs/subsystems/tools.md similarity index 69% rename from docs/core-data-structures/tools.md rename to docs/subsystems/tools.md index 853eed16cf..0551ee1c9d 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/subsystems/tools.md @@ -2,7 +2,7 @@ English | [中文](tools.zh.md) -The tool pipeline of [dsh-tools](../../packages/core/tools). [core.md](core.md) introduces `ToolDefinition` as the one pipeline-authoring type promoted to the spine and `ToolSchema` as the model-facing wire shape. This page owns the full `ToolDefinition`, the typed schema DSL that builds it, the guarded execution shapes, and the UI-presentation vocabulary. +The tool pipeline of [dsh-tools](../../packages/core/tools). [core.md](core.md) introduces `ToolDefinition` as the one pipeline-authoring type promoted to the spine; the model-facing [`ToolSchema`](llm-streaming.md#the-model-request-and-result) wire shape is declared with the model request. This page owns the full `ToolDefinition`, the typed schema DSL that builds it, the guarded execution shapes, and the UI-presentation vocabulary. Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts) · [`packages/core/tools/src/schema.ts`](../../packages/core/tools/src/schema.ts) · [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts) @@ -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](llm-streaming.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/core-data-structures/tools.zh.md b/docs/subsystems/tools.zh.md similarity index 69% rename from docs/core-data-structures/tools.zh.md rename to docs/subsystems/tools.zh.md index ec8809f6be..50843b07f1 100644 --- a/docs/core-data-structures/tools.zh.md +++ b/docs/subsystems/tools.zh.md @@ -2,7 +2,7 @@ [English](tools.md) | 中文 -[dsh-tools](../../packages/core/tools) 的工具流水线。[core.md](core.md) 介绍了 `ToolDefinition`(唯一被提升到主干的流水线编写类型)和 `ToolSchema`(面向模型的协议格式(wire format)形状)。本页拥有完整的 `ToolDefinition`、用于构建它的类型化 schema DSL、受保护的执行形状,以及 UI 展示词汇。 +[dsh-tools](../../packages/core/tools) 的工具流水线。[core.md](core.md) 介绍了 `ToolDefinition`(唯一被提升到主干的流水线编写类型);面向模型的 [`ToolSchema`](llm-streaming.md#the-model-request-and-result) 协议格式(wire format)形状与模型请求一起声明。本页拥有完整的 `ToolDefinition`、用于构建它的类型化 schema DSL、受保护的执行形状,以及 UI 展示词汇。 源码:[`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts) · [`packages/core/tools/src/schema.ts`](../../packages/core/tools/src/schema.ts) · [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts) @@ -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](llm-streaming.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 new file mode 100644 index 0000000000..b8fdb092f2 --- /dev/null +++ b/docs/subsystems/typert.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/typert.md +typert.md: 8ce52837ceee8e9c8c4a61de28f972912faa2cf0 +typert.zh.md: ff6b45a517c75392ee87fa348d72bfae6cdf1efb diff --git a/docs/core-data-structures/typert.md b/docs/subsystems/typert.md similarity index 68% rename from docs/core-data-structures/typert.md rename to docs/subsystems/typert.md index c70e50e2fe..8ce52837ce 100644 --- a/docs/core-data-structures/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/core-data-structures/typert.zh.md b/docs/subsystems/typert.zh.md similarity index 68% rename from docs/core-data-structures/typert.zh.md rename to docs/subsystems/typert.zh.md index 2cd1636d4c..ff6b45a517 100644 --- a/docs/core-data-structures/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/core-data-structures/code-runtime.i18n.yaml b/docs/subsystems/user-interaction.i18n.yaml similarity index 53% rename from docs/core-data-structures/code-runtime.i18n.yaml rename to docs/subsystems/user-interaction.i18n.yaml index 02bbc9587a..342cf9b9f7 100644 --- a/docs/core-data-structures/code-runtime.i18n.yaml +++ b/docs/subsystems/user-interaction.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/user-interaction.md +user-interaction.md: a19155ae06af0133ae468c004b2e3b66f1de3fb8 +user-interaction.zh.md: dc5845a976068981b2761e26502eaf7dd95dcb01 diff --git a/docs/core-data-structures/user-interaction.md b/docs/subsystems/user-interaction.md similarity index 69% rename from docs/core-data-structures/user-interaction.md rename to docs/subsystems/user-interaction.md index 73727cbb75..a19155ae06 100644 --- a/docs/core-data-structures/user-interaction.md +++ b/docs/subsystems/user-interaction.md @@ -2,9 +2,9 @@ English | [中文](user-interaction.zh.md) -The user-interaction seam of [dsh-user-interaction](../../packages/ui/user-interaction). It is the provider-neutral vocabulary a tool or permission plugin uses when it needs the human to answer before the agent can continue. UI surfaces provide the active `UserInteractionProvider`; the host runtime relays requests to its connected client. +The user-interaction seam of [dsh-user-interaction](../../packages/interaction/user-interaction). It is the provider-neutral vocabulary a tool or permission plugin uses when it needs the human to answer before the agent can continue. UI surfaces provide the active `UserInteractionProvider`; the host runtime relays requests to its connected client. -Source: [`packages/ui/user-interaction/src/index.ts`](../../packages/ui/user-interaction/src/index.ts) +Source: [`packages/interaction/user-interaction/src/index.ts`](../../packages/interaction/user-interaction/src/index.ts) ## Question options @@ -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/core-data-structures/user-interaction.zh.md b/docs/subsystems/user-interaction.zh.md similarity index 68% rename from docs/core-data-structures/user-interaction.zh.md rename to docs/subsystems/user-interaction.zh.md index b6e95af402..dc5845a976 100644 --- a/docs/core-data-structures/user-interaction.zh.md +++ b/docs/subsystems/user-interaction.zh.md @@ -2,9 +2,9 @@ [English](user-interaction.md) | 中文 -[dsh-user-interaction](../../packages/ui/user-interaction) 的用户交互 seam。它是工具或权限插件需要人类回答后 agent(智能体)才能继续时所使用的、提供方无关的词汇。UI surface 提供活跃的 `UserInteractionProvider`;host 运行时把请求转发给它连接的客户端。 +[dsh-user-interaction](../../packages/interaction/user-interaction) 的用户交互 seam。它是工具或权限插件需要人类回答后 agent(智能体)才能继续时所使用的、提供方无关的词汇。UI surface 提供活跃的 `UserInteractionProvider`;host 运行时把请求转发给它连接的客户端。 -源码:[`packages/ui/user-interaction/src/index.ts`](../../packages/ui/user-interaction/src/index.ts) +源码:[`packages/interaction/user-interaction/src/index.ts`](../../packages/interaction/user-interaction/src/index.ts) ## 问题选项 @@ -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/core-data-structures/web.i18n.yaml b/docs/subsystems/web.i18n.yaml similarity index 57% rename from docs/core-data-structures/web.i18n.yaml rename to docs/subsystems/web.i18n.yaml index e8ae658d3a..f3f0ab9758 100644 --- a/docs/core-data-structures/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/core-data-structures/web.md b/docs/subsystems/web.md similarity index 73% rename from docs/core-data-structures/web.md rename to docs/subsystems/web.md index 20d07240c9..f300bf2dae 100644 --- a/docs/core-data-structures/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/core-data-structures/web.zh.md b/docs/subsystems/web.zh.md similarity index 74% rename from docs/core-data-structures/web.zh.md rename to docs/subsystems/web.zh.md index e2982ba571..0c72e343c1 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..4f581d8bae --- /dev/null +++ b/docs/subsystems/workflow.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/workflow.md +workflow.md: 53cb8be2c975fba396c3d2023c60fc1da2ed9c8a +workflow.zh.md: 652b68c833f7f20fe72307946d02f89558be2c2b diff --git a/docs/core-data-structures/workflow.md b/docs/subsystems/workflow.md similarity index 55% rename from docs/core-data-structures/workflow.md rename to docs/subsystems/workflow.md index 8d271b89e7..53cb8be2c9 100644 --- a/docs/core-data-structures/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/core-data-structures/workflow.zh.md b/docs/subsystems/workflow.zh.md similarity index 55% rename from docs/core-data-structures/workflow.zh.md rename to docs/subsystems/workflow.zh.md index 91a4902bbc..652b68c833 100644 --- a/docs/core-data-structures/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 new file mode 100644 index 0000000000..b2c2248876 --- /dev/null +++ b/docs/subsystems/workspace.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/workspace.md +workspace.md: ca088a2091a7f47a3d52992fec13fae44061a608 +workspace.zh.md: e414c759a043f934e1a8b5d89c7a3b6101bbb6f4 diff --git a/docs/subsystems/workspace.md b/docs/subsystems/workspace.md new file mode 100644 index 0000000000..ca088a2091 --- /dev/null +++ b/docs/subsystems/workspace.md @@ -0,0 +1,219 @@ +# Workspaces + +English | [中文](workspace.zh.md) + +A workspace is the persistent record of a directory the user works in: a stable id over a canonical path, a display title, and the ordered account of sessions that belong to it. The subsystem is one package ([dsh-workspace](../../packages/workspace/workspace), `ctx.workspace`) — an optional host-side capability, not part of the agent-loop spine, and invisible to models (no tools, no prompt text, no session events). It stores its records through the [storage domain form](storage.md) and validates session membership against [`SessionHeader.cwd`](persistence.md#sessionheader--metadata-beside-the-log), so `storageDomain` and `sessionPersistence` are mandatory startup dependencies: an unavailable persistence peer leaves the plugin pending rather than being mistaken for an empty history. Design record: [domain KV storage Agent Note](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md); bootstrap and GUI ordering: [Workspace UI product-flow Agent Note](../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.md). + +Source: [`packages/workspace/workspace/src/types.ts`](../../packages/workspace/workspace/src/types.ts) + +## Identity + +```ts type-equiv +/** + * Identifies one workspace record. A generated uuid, never the path: path + * normalization rewrites paths, and a reference anchor must stay stable. + */ +type WorkspaceId = Branded<'WorkspaceId'> +``` + +`WorkspaceId` is a [branded id](core.md#branded-ids). Path identity is separate: `realpathNormalize` (`fs.realpath`; trailing slashes, `..`, and symlinks resolved) is the one uniqueness canon — workspace paths are stored canonicalized, uniqueness is string equality of canonical paths (a symlink to an owned directory collides), and attach-time session cwd checks go through the same canon. + +## The workspace entity + +Consumers see only the `Workspace` interface; the implementation stays package-private. + +```ts type-equiv +/** + * One workspace: a stable id over an existing directory, a display title, and + * an ordered candidate account of sessions. Membership requires both an id in + * that account and a session header whose canonical cwd equals the workspace + * path. Consumers only see this interface; the implementation stays private. + */ +interface Workspace { + /** Stable record id (generated uuid). */ + readonly id: WorkspaceId + + /** + * Canonical directory path: the `fs.realpath` of the path given at create + * time (trailing slashes, `..`, and symlinks all resolved). Never rewritten + * afterwards, even when the directory disappears (see {@link status}). + */ + readonly path: string + + /** Display title. Defaults to `basename(path)` at create; duplicates are allowed. */ + readonly title: string + + /** ISO-8601 creation instant, stamped at create and never rewritten. */ + readonly createdAt: string + + /** ISO-8601 instant of the last durable mutation (create counts as one). */ + readonly updatedAt: string + + /** + * Header-validated sessions in manually owned order: a new session is + * prepended at attach, explicit reordering goes through + * `insertSessionBefore`, and activity never reorders. The durable candidate + * account is filtered synchronously: missing headers, invalid cwd values, + * and canonical cwd mismatches are never returned. A subsequent workspace + * mutation prunes those filtered candidates durably. + */ + readonly sessionIds: readonly SessionId[] + + /** + * Replace the display title durably. + * @param title - New title; any string, duplicates across workspaces allowed. + * @returns resolution after durability. + */ + setTitle(title: string): Promise + + /** + * Prepend a session to this workspace's candidate account. An already + * accounted id resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs. A new id's + * live or persisted + * header cwd must resolve to an existing directory equal to {@link path}; + * unknown ids, missing or invalid cwd values, and mismatches reject without + * writing. + * @param sessionId - The session to record. + * @returns resolution after durability. + */ + attachSession(sessionId: SessionId): Promise + + /** + * Move an accounted session within the manual order, DOM-insertBefore-like: + * with an anchor the session lands before it, without one it appends to the + * end. Only the moved id changes position. A session or anchor absent from + * the account rejects without writing; a move to the current position + * resolves without writing, aside from the durable filtered-candidate + * prune every accepted mutation performs; decided on the domain write + * chain. + * @param sessionId - The accounted session to move. + * @param beforeSessionId - Accounted anchor to insert before; omitted appends. + * @returns resolution after durability. + */ + insertSessionBefore(sessionId: SessionId, beforeSessionId?: SessionId): Promise + + /** + * Remove a session from this workspace's account. Idempotent: an id not on + * the account resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs; decided on + * the domain write chain like attach. Never touches the session's own stored log. + * @param sessionId - The session to remove. + * @returns resolution after durability. + */ + detachSession(sessionId: SessionId): Promise + + /** + * Live directory check, uncached: whether {@link path} currently exists and + * is a directory. A missing directory never mutates the record — the + * directory may only be temporarily moved. + * @returns `'ok'` when the directory exists, `'missing-dir'` otherwise. + */ + status(): Promise<'ok' | 'missing-dir'> +} +``` + +Ownership truth is the record's ordered `sessionIds`, never derived from session cwd — but membership requires both: an id on the account and a header whose canonical cwd equals the workspace path, so one session structurally belongs to at most one workspace. Failed writes reject (`insertSessionBefore` account errors as `WorkspaceMoveInvalidError`, storage failures as plain errors); every accepted mutation stamps `updatedAt` and durably prunes candidates that no longer pass the membership check. + +## The registry: `ctx.workspace` + +`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 new file mode 100644 index 0000000000..e414c759a0 --- /dev/null +++ b/docs/subsystems/workspace.zh.md @@ -0,0 +1,219 @@ +# 工作区 + +[English](workspace.md) | 中文 + +工作区(workspace)是用户工作目录的持久记录:一个建立在规范路径之上的稳定 id、一个显示标题,以及归属于它的会话的有序账本。该子系统是单个包(package)([dsh-workspace](../../packages/workspace/workspace),`ctx.workspace`)——一项宿主侧可选能力,不属于 agent loop(智能体循环)主干,并且对模型不可见(没有工具、没有提示词文本、没有会话事件)。它通过[存储领域数据形式](storage.md)存储自己的记录,并对照 [`SessionHeader.cwd`](persistence.md#sessionheader--metadata-beside-the-log) 校验会话成员资格,因此 `storageDomain` 与 `sessionPersistence` 是必需的启动依赖:持久化这一依赖不可用时,插件保持 pending,而不是把这种不可用误当作空历史。设计记录:[领域 KV 存储 Agent Note(agent 决策记录)](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md);引导与 GUI 顺序:[Workspace UI 产品流程 Agent Note](../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.md)。 + +源码:[`packages/workspace/workspace/src/types.ts`](../../packages/workspace/workspace/src/types.ts) + +## 标识 + +```ts type-equiv +/** + * Identifies one workspace record. A generated uuid, never the path: path + * normalization rewrites paths, and a reference anchor must stay stable. + */ +type WorkspaceId = Branded<'WorkspaceId'> +``` + +`WorkspaceId` 是[品牌化 id](core.md#branded-ids)。路径标识与之分离:`realpathNormalize`(`fs.realpath`;尾部斜杠、`..` 与符号链接全部解析)是唯一的一套唯一性规范——工作区路径以规范化形式存储,唯一性即规范路径的字符串相等(指向已被拥有目录的符号链接会与之冲突),attach 时的会话 cwd 检查也走同一套规范。 + +## 工作区实体 + +消费方只看到 `Workspace` 接口;实现保持包内私有。 + +```ts type-equiv +/** + * One workspace: a stable id over an existing directory, a display title, and + * an ordered candidate account of sessions. Membership requires both an id in + * that account and a session header whose canonical cwd equals the workspace + * path. Consumers only see this interface; the implementation stays private. + */ +interface Workspace { + /** Stable record id (generated uuid). */ + readonly id: WorkspaceId + + /** + * Canonical directory path: the `fs.realpath` of the path given at create + * time (trailing slashes, `..`, and symlinks all resolved). Never rewritten + * afterwards, even when the directory disappears (see {@link status}). + */ + readonly path: string + + /** Display title. Defaults to `basename(path)` at create; duplicates are allowed. */ + readonly title: string + + /** ISO-8601 creation instant, stamped at create and never rewritten. */ + readonly createdAt: string + + /** ISO-8601 instant of the last durable mutation (create counts as one). */ + readonly updatedAt: string + + /** + * Header-validated sessions in manually owned order: a new session is + * prepended at attach, explicit reordering goes through + * `insertSessionBefore`, and activity never reorders. The durable candidate + * account is filtered synchronously: missing headers, invalid cwd values, + * and canonical cwd mismatches are never returned. A subsequent workspace + * mutation prunes those filtered candidates durably. + */ + readonly sessionIds: readonly SessionId[] + + /** + * Replace the display title durably. + * @param title - New title; any string, duplicates across workspaces allowed. + * @returns resolution after durability. + */ + setTitle(title: string): Promise + + /** + * Prepend a session to this workspace's candidate account. An already + * accounted id resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs. A new id's + * live or persisted + * header cwd must resolve to an existing directory equal to {@link path}; + * unknown ids, missing or invalid cwd values, and mismatches reject without + * writing. + * @param sessionId - The session to record. + * @returns resolution after durability. + */ + attachSession(sessionId: SessionId): Promise + + /** + * Move an accounted session within the manual order, DOM-insertBefore-like: + * with an anchor the session lands before it, without one it appends to the + * end. Only the moved id changes position. A session or anchor absent from + * the account rejects without writing; a move to the current position + * resolves without writing, aside from the durable filtered-candidate + * prune every accepted mutation performs; decided on the domain write + * chain. + * @param sessionId - The accounted session to move. + * @param beforeSessionId - Accounted anchor to insert before; omitted appends. + * @returns resolution after durability. + */ + insertSessionBefore(sessionId: SessionId, beforeSessionId?: SessionId): Promise + + /** + * Remove a session from this workspace's account. Idempotent: an id not on + * the account resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs; decided on + * the domain write chain like attach. Never touches the session's own stored log. + * @param sessionId - The session to remove. + * @returns resolution after durability. + */ + detachSession(sessionId: SessionId): Promise + + /** + * Live directory check, uncached: whether {@link path} currently exists and + * is a directory. A missing directory never mutates the record — the + * directory may only be temporarily moved. + * @returns `'ok'` when the directory exists, `'missing-dir'` otherwise. + */ + status(): Promise<'ok' | 'missing-dir'> +} +``` + +所有权的真源是记录中有序的 `sessionIds`,绝不从会话 cwd 派生——但成员资格要求两者同时成立:账本上有其 id,且 header 的规范 cwd 等于工作区路径,因此一个会话在结构上至多属于一个工作区。失败的写入会拒绝(`insertSessionBefore` 的账本错误以 `WorkspaceMoveInvalidError` 拒绝,存储失败以普通错误拒绝);每次被接受的变更都盖上 `updatedAt` 时间戳,并持久修剪不再通过成员资格检查的候选项。 + +## 注册表:`ctx.workspace` + +`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/testing.i18n.yaml b/docs/testing.i18n.yaml index 90970c8c57..0b7cdfdc76 100644 --- a/docs/testing.i18n.yaml +++ b/docs/testing.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/testing.md -testing.md: fecf02fe887aac5c92c739523f10a0562eba6242 -testing.zh.md: a34f56fac72baa9916d5f96e83048ad54cf23d4a +testing.md: 296dd769118cc5498ddb1bb9c41e2c060b2a5bce +testing.zh.md: e9182495fbdbb1458c9ff5b30a9591f8b70f27c5 diff --git a/docs/testing.md b/docs/testing.md index fecf02fe88..296dd76911 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -32,7 +32,7 @@ An e2e assertion re-runs the command or re-reads the file externally; a keyword - Product-visible plugins require a non-unit REAL-composition test. Hand-built `ctx.plugin(...)` suites are insufficient: boot test-only `cordis.yml` through Loader and app/process, mock only external/nondeterministic boundaries, and assert model-visible request/log, durable state, or user-visible output. Keep opt-ins out of shipped defaults. - A guard only guards if the regression actually fails it. For a plugin without `inject` (bundle/composition plugins), a Loader smoke stays green under a broken export shape — add an explicit `expect('default' in mod).toBe(false)` plus an `unwrapExports` round-trip assertion, and prove it: introduce the regression, watch red, revert. -- "Real entry path" means the published artifact: a package `bin` runs built `lib/bin.js` under plain `node`, exposing failures tsx masks (settle races, module resolution, swallowed load failures). The same applies to non-index runtime entries (the worker-thread sibling `lib/worker.cjs`) and singleton modules shared across bundles (`packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts`). Keep the built-artifact smokes green (`packages/ui/*/tests/built-bin.e2e.ts`, `packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`), and assert a genuinely-missing config exits non-zero. +- "Real entry path" means the published artifact: a package `bin` runs built `lib/bin.js` under plain `node`, exposing failures tsx masks (settle races, module resolution, swallowed load failures). The same applies to non-index runtime entries (the worker-thread sibling `lib/worker.cjs`) and singleton modules shared across bundles (`packages/scaffold/server/tests/built-scope-carrier.e2e.ts`). Keep the built-artifact smokes green (`packages/examples/*/tests/built-bin.e2e.ts`, `packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`), and assert a genuinely-missing config exits non-zero. ## Test resolution: source plane only diff --git a/docs/testing.zh.md b/docs/testing.zh.md index a34f56fac7..e9182495fb 100644 --- a/docs/testing.zh.md +++ b/docs/testing.zh.md @@ -32,7 +32,7 @@ e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身 - 产品可见的插件必须有一个非单元的真实组合测试。手动构建的 `ctx.plugin(...)` 套件不够:通过 Loader 和 app/process 启动仅用于测试的 `cordis.yml`,只 mock 外部/不确定边界,断言模型可见的请求/日志、持久状态或用户可见输出。不要把 opt-in 选项混入交付默认值。 - 一个守卫只有在回归真的能让它失败时才有效。对于没有 `inject` 的插件(bundle/组合插件),Loader 冒烟测试在导出形状损坏时仍然绿着——需要添加显式的 `expect('default' in mod).toBe(false)` 加 `unwrapExports` 往返断言,并证明它有效:引入回归、观察变红、回退。 -- 「真实入口路径」指已发布的产物:包的 `bin` 所运行的是构建后的 `lib/bin.js`,并由普通 `node` 执行,从而暴露 tsx 会掩盖的失败(等待稳定时的竞态、模块解析、被吞掉的加载失败)。同样的规则适用于非 index 运行时入口(worker-thread 的同级文件 `lib/worker.cjs`),也适用于多个 bundle 共享的单例模块(`packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts`)。保持构建产物冒烟测试绿色(`packages/ui/*/tests/built-bin.e2e.ts`、`packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`),并断言真正缺失的配置以非零状态退出。 +- 「真实入口路径」指已发布的产物:包的 `bin` 所运行的是构建后的 `lib/bin.js`,并由普通 `node` 执行,从而暴露 tsx 会掩盖的失败(等待稳定时的竞态、模块解析、被吞掉的加载失败)。同样的规则适用于非 index 运行时入口(worker-thread 的同级文件 `lib/worker.cjs`),也适用于多个 bundle 共享的单例模块(`packages/scaffold/server/tests/built-scope-carrier.e2e.ts`)。保持构建产物冒烟测试绿色(`packages/examples/*/tests/built-bin.e2e.ts`、`packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`),并断言真正缺失的配置以非零状态退出。 ## 测试解析:仅限源码 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index adb73f104d..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 [core-data-structures/](core-data-structures/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). @@ -107,7 +107,7 @@ Ask the user a concise question when you need confirmation, a choice, or missing } ``` -Source: [`packages/ui/tool-ask-user/src/index.ts`](../packages/ui/tool-ask-user/src/index.ts) +Source: [`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts) ask_user_question pauses the tool call until the active UI provider returns a human answer. @@ -280,7 +280,7 @@ Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: } ``` -Source: [`packages/cordis/tool-cordis/src/index.ts`](../packages/cordis/tool-cordis/src/index.ts) +Source: [`packages/self-modification/tool-cordis/src/index.ts`](../packages/self-modification/tool-cordis/src/index.ts) ### `cordis_mount` @@ -301,7 +301,7 @@ Mount a temporary Cordis Plugin in the current DSH process. This creates an in-m } ``` -Source: [`packages/cordis/tool-cordis/src/index.ts`](../packages/cordis/tool-cordis/src/index.ts) +Source: [`packages/self-modification/tool-cordis/src/index.ts`](../packages/self-modification/tool-cordis/src/index.ts) ### `cordis_unmount` @@ -322,7 +322,7 @@ Unmount a current-process temporary Plugin created by cordis_mount. Waits for it } ``` -Source: [`packages/cordis/tool-cordis/src/index.ts`](../packages/cordis/tool-cordis/src/index.ts) +Source: [`packages/self-modification/tool-cordis/src/index.ts`](../packages/self-modification/tool-cordis/src/index.ts) Not in any shipped tree (a deliberate opt-in — temporary Plugin code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins created by cordis_mount may register ADDITIONAL model-visible tools until unmounted or DSH restarts; a full changed request header logs those tool-set changes. 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/examples/web-cordis/README.i18n.yaml b/examples/web-cordis/README.i18n.yaml index 0564a4e0fc..86cc276a4e 100644 --- a/examples/web-cordis/README.i18n.yaml +++ b/examples/web-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 examples/web-cordis/README.md -README.md: 75c7610fecc20777607c7c8cbb6ed2fc7f590b73 -README.zh.md: 5c25347ee17037bbfc7d10a5507424ce52e3faff +README.md: 21fe0a210b2e591a96dc254014a0f91ed9afa2ba +README.zh.md: 35158affd5cbdd6f8fa7f5910500f21c2b241309 diff --git a/examples/web-cordis/README.md b/examples/web-cordis/README.md index 75c7610fec..21fe0a210b 100644 --- a/examples/web-cordis/README.md +++ b/examples/web-cordis/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Self-referential demonstration of [`@deepseek-ai/dsh-tool-cordis`](../../packages/cordis/tool-cordis/README.md). The agent can inspect its current Cordis process and mount or unmount model-authored plugins in memory. Temporary plugins disappear when they are unmounted or the process exits and may affect other sessions in the same process. +Self-referential demonstration of [`@deepseek-ai/dsh-tool-cordis`](../../packages/self-modification/tool-cordis/README.md). The agent can inspect its current Cordis process and mount or unmount model-authored plugins in memory. Temporary plugins disappear when they are unmounted or the process exits and may affect other sessions in the same process. ## Run it @@ -18,4 +18,4 @@ Start the ACP automation server instead: pnpm run demo:cordis acp ``` -Both commands require `DEEPSEEK_API_KEY`. The [Cordis tool reference](../../packages/cordis/tool-cordis/README.md) owns the tool, lifecycle, and safety contracts. +Both commands require `DEEPSEEK_API_KEY`. The [Cordis tool reference](../../packages/self-modification/tool-cordis/README.md) owns the tool, lifecycle, and safety contracts. diff --git a/examples/web-cordis/README.zh.md b/examples/web-cordis/README.zh.md index 5c25347ee1..35158affd5 100644 --- a/examples/web-cordis/README.zh.md +++ b/examples/web-cordis/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -[`@deepseek-ai/dsh-tool-cordis`](../../packages/cordis/tool-cordis/README.md) 的自指示例。agent(智能体)可以检查当前 Cordis 进程,并在内存中挂载或卸载模型编写的插件。临时插件会在卸载或进程退出时消失,并可能影响同一进程中的其他会话。 +[`@deepseek-ai/dsh-tool-cordis`](../../packages/self-modification/tool-cordis/README.md) 的自指示例。agent(智能体)可以检查当前 Cordis 进程,并在内存中挂载或卸载模型编写的插件。临时插件会在卸载或进程退出时消失,并可能影响同一进程中的其他会话。 ## 运行 @@ -18,4 +18,4 @@ pnpm run demo:cordis pnpm run demo:cordis acp ``` -这两条命令都需要 `DEEPSEEK_API_KEY`。工具、生命周期和安全契约由 [Cordis 工具参考](../../packages/cordis/tool-cordis/README.md)定义。 +这两条命令都需要 `DEEPSEEK_API_KEY`。工具、生命周期和安全契约由 [Cordis 工具参考](../../packages/self-modification/tool-cordis/README.md)定义。 diff --git a/knip.json b/knip.json index d154cd646b..63db33e51c 100644 --- a/knip.json +++ b/knip.json @@ -283,7 +283,7 @@ "tests/**/*.ts" ] }, - "packages/telemetry/session-telemetry-otel": { + "packages/session/session-telemetry-otel": { "entry": [ "tests/**/*.spec.ts", "tests/**/*.e2e.ts" @@ -414,7 +414,7 @@ "tests/**/*.ts" ] }, - "packages/session-title/session-title-first-message-llm": { + "packages/session/session-title-first-message-llm": { "entry": [ "tests/**/*.spec.ts", "tests/**/*.e2e.ts" @@ -434,7 +434,7 @@ "tests/**/*.ts" ] }, - "packages/session-persistence/session-checkpoint-policy": { + "packages/session/session-checkpoint-policy": { "entry": [ "tests/**/*.spec.ts", "tests/**/*.e2e.ts" @@ -513,7 +513,7 @@ "tests/**/*.ts" ] }, - "packages/ui/jsonrpc": { + "packages/scaffold/server": { "entry": [ "tests/**/*.spec.ts", "tests/**/*.e2e.ts" @@ -523,7 +523,7 @@ "tests/**/*.ts" ] }, - "packages/ui/commands": { + "packages/interaction/commands": { "entry": [ "tests/**/*.spec.ts" ], @@ -537,7 +537,7 @@ "src/**/*.ts" ] }, - "packages/sdk/create-sdk": { + "packages/scaffold/create-sdk": { "entry": [ "src/bin.ts", "tests/**/*.spec.ts", @@ -549,7 +549,7 @@ "tests/**/*.ts" ] }, - "packages/sdk/scripts": { + "packages/scaffold/scripts": { "entry": [ "src/bin.ts", "tests/**/*.spec.ts", diff --git a/lefthook.yml b/lefthook.yml index c2249adbde..0e78d16252 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -4,6 +4,10 @@ pre-commit: jobs: + - name: translation pairing (staged records) + glob: '*.i18n.yaml' + run: node_modules/.bin/tsx scripts/verify-translation-pairing.ts --cached {staged_files} + - name: format (staged) glob: '*.{ts,tsx,mts,cts,mjs}' exclude: @@ -34,6 +38,12 @@ pre-commit: - name: vendor manifest guard run: scripts/check-vendor-manifest.sh +pre-merge-commit: + jobs: + - name: translation pairing (staged records) + glob: '*.i18n.yaml' + run: node_modules/.bin/tsx scripts/verify-translation-pairing.ts --cached {staged_files} + pre-push: jobs: - name: typecheck diff --git a/package.json b/package.json index 423cbf813c..864814ffe6 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "verify-type-equiv": "tsx scripts/verify-type-equiv.ts", "verify-translation-prompt": "tsx scripts/verify-translation-prompt.ts", "verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts", + "resolve-translation-pairing-conflicts": "tsx scripts/merge-translation-pairing.ts --resolve", "gen-translation-brief": "tsx scripts/gen-translation-brief.ts", "verify-doc-budgets": "tsx scripts/verify-doc-budgets.ts", "docs:dev": "pnpm --filter @deepseek-ai/website run dev", diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index 9d1ef06532..8f96dd96a3 100644 --- a/packages/README.i18n.yaml +++ b/packages/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/README.md -README.md: d1e579633ea597c46003af97ab9a47e5e616ca73 -README.zh.md: aa6577413899627a947b446ef8f6545492c880cb +README.md: 07e6819c9b26eb7a46d6f67e90a939e9cf76069c +README.zh.md: 01b191464edde24bce89c930008e348b3da348b0 diff --git a/packages/README.md b/packages/README.md index d1e579633e..07e6819c9b 100644 --- a/packages/README.md +++ b/packages/README.md @@ -35,23 +35,20 @@ Groups hold `packages///`; names stay `@deepseek-ai/dsh-`. **Gr | [`todo/`](todo/README.md) | The model-facing `todo_write` tool | Product — stable surface | | [`plan/`](plan/README.md) | Plan collaboration state with a direct entry command and reviewed exit | Product — stable surface | | [`preset/`](preset/README.md) | Per-session agent composition from preset `cordis.yml` files | Product — stable surface | -| [`timeout/`](timeout/README.md) | Tool-call `tools/execute` deadline enforcement | Product — stable surface | -| [`guard/`](guard/README.md) | Loop-hygiene advisory repeat-call reminders | Product — stable surface | +| [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders + the `tools/execute` deadline enforcer | Product — stable surface | | [`bundle/`](bundle/README.md) | Installable `dsh --profile` patch layers | Product — stable surface | -| [`cordis/`](cordis/README.md) | Cordis runtime integration: self-inspection, temporary Plugins, restricted repository Plugin loading | Product — stable surface | +| [`self-modification/`](self-modification/README.md) | The agent modifies its own runtime: inspect the live runtime's plugins and services, mount/unmount model-written plugins ([design](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) and restricted repository Plugin loading | Product — stable surface | | [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface | -| [`session-persistence/`](session-persistence/README.md) | Persistence seam + JSONL/SQLite backends | Product — stable surface | -| [`session-projection/`](session-projection/README.md) | Projection seam: domain fold units serve whole values | Product — stable surface | +| [`session/`](session/README.md) | Durable session data plane: persistence seam + JSONL/SQLite backends, projection seam, log-backed titles, session reporting | Product — stable surface | | [`session-query/`](session-query/README.md) | Session retrieval family: logical corpus, bounded reads, lineage, event relationships, semantic filtering, and SQLite full-text search | Product — stable surface | -| [`session-title/`](session-title/README.md) | Log-backed session titles: fallback service and opt-in LLM providers | Product — stable surface | | [`settings/`](settings/README.md) | User-settings seam + file-backed provider | Product — stable surface | | [`credentials/`](credentials/README.md) | Credential-reference seam + env-over-`.env` provider | Product — stable surface | -| [`telemetry/`](telemetry/README.md) | Session reporting: capture/redact seam, OTel backend | Product — stable surface | | [`storage/`](storage/README.md) | Non-session storage hub + backends + domain form | Product — stable surface | | [`workspace/`](workspace/README.md) | Workspace entity | Product — stable surface | -| [`sdk/`](sdk/README.md) | Project SDK tooling | Product — stable surface | +| [`scaffold/`](scaffold/README.md) | Create/launch/drive project tooling: helper, launcher, initializer, wire protocol with both ends, launcher telemetry | Product — stable surface | | [`acp/`](acp/README.md) | Automation-only Agent Client Protocol server | Product — stable surface | -| [`ui/`](ui/README.md) | JSON-RPC integration, approval/interaction seams, ask-user tool | Product — stable surface | +| [`interaction/`](interaction/README.md) | Human-collaboration plane: approval/interaction seams, permission preset, commands, ask-user tool | Product — stable surface | +| [`boot/`](boot/README.md) | Shared app-bin boot glue | Product — stable surface | | [`host/`](host/README.md) | Web-GUI host half: API gateway + HTTP route server | Product — stable surface | | [`client/`](client/README.md) | Web-GUI browser half: shell, wire, object services, slots, `ui-*` plugins | Product — stable surface | | [`experimental/`](experimental/README.md) | Prototypes and internal plugins | Unreleased | diff --git a/packages/README.zh.md b/packages/README.zh.md index aa65774138..01b191464e 100644 --- a/packages/README.zh.md +++ b/packages/README.zh.md @@ -35,23 +35,20 @@ | [`todo/`](todo/README.md) | 面向模型的 `todo_write` 工具 | 产品:稳定表面 | | [`plan/`](plan/README.md) | Plan 协作状态,提供直接进入命令与经评审的退出 | 产品:稳定表面 | | [`preset/`](preset/README.md) | 由 preset `cordis.yml` 按会话组装 agent | 产品:稳定表面 | -| [`timeout/`](timeout/README.md) | 工具调用 `tools/execute` 截止时间强制执行 | 产品:稳定表面 | -| [`guard/`](guard/README.md) | 循环卫生建议性重复调用提醒 | 产品:稳定表面 | +| [`guard/`](guard/README.md) | 循环卫生守卫:建议性重复调用提醒 + `tools/execute` 截止时间强制执行器 | 产品:稳定表面 | | [`bundle/`](bundle/README.md) | 可安装的 `dsh --profile` 补丁层 | 产品:稳定表面 | -| [`cordis/`](cordis/README.md) | Cordis 运行时集成:自检、临时 Plugin、受限 repository Plugin 加载 | 产品:稳定表面 | +| [`self-modification/`](self-modification/README.md) | agent 修改自身运行时:检查实时运行时的插件与服务,挂载/卸载模型所写插件([设计](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)),以及受限 repository Plugin 加载 | 产品:稳定表面 | | [`hooks/`](hooks/README.md) | 钩子桥接 + 共享 Claude Code/Codex 协议格式库 | 产品:稳定表面 | -| [`session-persistence/`](session-persistence/README.md) | 持久化 seam + JSONL/SQLite 后端 | 产品:稳定表面 | -| [`session-projection/`](session-projection/README.md) | 投影 seam:领域折叠单元供给全量值 | 产品:稳定表面 | +| [`session/`](session/README.md) | 持久会话数据平面:持久化 seam + JSONL/SQLite 后端、投影 seam、日志支持的标题、会话上报 | 产品:稳定表面 | | [`session-query/`](session-query/README.md) | 会话检索系列:逻辑语料库、有界读取、血缘、事件关系、语义过滤和 SQLite 全文搜索 | 产品:稳定表面 | -| [`session-title/`](session-title/README.md) | 日志支撑的会话标题:回退服务与选用 LLM 提供方 | 产品:稳定表面 | | [`settings/`](settings/README.md) | 用户设置 seam + 文件 provider | 产品:稳定表面 | | [`credentials/`](credentials/README.md) | 凭据引用 seam + 环境叠加 `.env` provider | 产品:稳定表面 | -| [`telemetry/`](telemetry/README.md) | 会话上报:捕获/脱敏 seam、OTel 后端 | 产品:稳定表面 | | [`storage/`](storage/README.md) | 非会话存储中枢 + 后端 + 领域形式 | 产品:稳定表面 | | [`workspace/`](workspace/README.md) | Workspace 实体 | 产品:稳定表面 | -| [`sdk/`](sdk/README.md) | 项目 SDK 工具 | 产品:稳定表面 | +| [`scaffold/`](scaffold/README.md) | 创建/启动/驱动项目的工具:helper、启动器、初始化器、带两端的通信协议、启动器 telemetry | 产品:稳定表面 | | [`acp/`](acp/README.md) | 仅面向自动化的 Agent Client Protocol 服务器 | 产品:稳定表面 | -| [`ui/`](ui/README.md) | JSON-RPC 集成、批准/交互 seam、用户问答工具 | 产品:稳定表面 | +| [`interaction/`](interaction/README.md) | 人机协作平面:批准/交互 seam、权限预设、命令、用户问答工具 | 产品:稳定表面 | +| [`boot/`](boot/README.md) | 共享的 app bin 启动粘合层 | 产品:稳定表面 | | [`host/`](host/README.md) | web GUI 宿主半侧:API 网关 + HTTP 路由服务器 | 产品:稳定表面 | | [`client/`](client/README.md) | web GUI 浏览器半侧:shell、协议层、对象服务、slot、`ui-*` 插件 | 产品:稳定表面 | | [`experimental/`](experimental/README.md) | 原型和内部插件 | 未发布 | diff --git a/packages/acp/README.i18n.yaml b/packages/acp/README.i18n.yaml index ca4ec66f67..cd4555752b 100644 --- a/packages/acp/README.i18n.yaml +++ b/packages/acp/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/acp/README.md -README.md: 3ba247598f29f2244456061fe7f9a3282086148f -README.zh.md: c13bc05b12fff2ef97b4d547936aa14a6556430a +README.md: 97af6d164b265bf0e98e3c9f5a444cffad4face5 +README.zh.md: 266b108c108b9f543d93cbb727ddbc01ab4bef36 diff --git a/packages/acp/README.md b/packages/acp/README.md index 3ba247598f..97af6d164b 100644 --- a/packages/acp/README.md +++ b/packages/acp/README.md @@ -2,10 +2,10 @@ English | [中文](README.zh.md) -The ACP group exposes harness agents to programmatic clients. It is an interoperability transport, not a presentation or human-interaction layer. +The ACP group exposes harness agents to programmatic clients over the Agent Client Protocol. It is an interoperability transport, not a presentation or human-interaction layer; the matching out-of-process subagent *client* lives in [`subagent/subagent-acp`](../subagent/subagent-acp/README.md) because it implements the subagent provider interface. | Package | Role | |---|---| | [`acp/`](acp/README.md) | Automation-only ACP server. | -The matching out-of-process subagent client remains in [`subagent/subagent-acp`](../subagent/subagent-acp/README.md) because it implements the subagent provider interface; arbitrary ACP clients may drive the same server contract. +The server contract is documented in [`acp/README.md`](acp/README.md). diff --git a/packages/acp/README.zh.md b/packages/acp/README.zh.md index c13bc05b12..266b108c10 100644 --- a/packages/acp/README.zh.md +++ b/packages/acp/README.zh.md @@ -2,10 +2,10 @@ [English](README.md) | 中文 -ACP(Agent Client Protocol)组将 harness 中的 agent(智能体)公开给程序化客户端。它是互操作传输层,而非展示层或人机交互层。 +ACP 组通过 Agent Client Protocol 把 harness agent 暴露给编程客户端。它是互操作传输层,不是展示或人机交互层;配对的进程外 subagent *客户端*在 [`subagent/subagent-acp`](../subagent/subagent-acp/README.md),因为它实现的是 subagent 提供方接口。 | 包 | 职责 | |---|---| | [`acp/`](acp/README.md) | 仅面向自动化的 ACP 服务器。 | -与之匹配的进程外 subagent 客户端仍位于 [`subagent/subagent-acp`](../subagent/subagent-acp/README.md),因为它实现 subagent 提供方接口;任意 ACP 客户端都可以按照同一服务器契约驱动该服务器。 +服务器契约见 [`acp/README.md`](acp/README.md)。 diff --git a/packages/acp/acp/tsconfig.json b/packages/acp/acp/tsconfig.json index 3109e0eea8..177b3cb3cd 100644 --- a/packages/acp/acp/tsconfig.json +++ b/packages/acp/acp/tsconfig.json @@ -24,7 +24,7 @@ "path": "../../core/agent" }, { - "path": "../../ui/user-approval" + "path": "../../interaction/user-approval" }, { "path": "../../support/invariants" diff --git a/packages/api/remotes/tsconfig.host.json b/packages/api/remotes/tsconfig.host.json index 1d4c35a9e2..1082a0d94b 100644 --- a/packages/api/remotes/tsconfig.host.json +++ b/packages/api/remotes/tsconfig.host.json @@ -21,7 +21,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { "path": "../../support/invariants" diff --git a/packages/bash/README.i18n.yaml b/packages/bash/README.i18n.yaml index 4c4a2c6180..00321c8ab0 100644 --- a/packages/bash/README.i18n.yaml +++ b/packages/bash/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/bash/README.md -README.md: 601782caad24d3555a206365a3f1954d81af1cf0 -README.zh.md: 8b96c4f80ba8776bfd8bdde178178cea946ff36c +README.md: 7395a051742af24b96842966b71297dc01853dca +README.zh.md: 1d5825e077c46284dbd4895e285cda8f39c252a3 diff --git a/packages/bash/README.md b/packages/bash/README.md index 601782caad..7395a05174 100644 --- a/packages/bash/README.md +++ b/packages/bash/README.md @@ -15,3 +15,5 @@ The capability family spans the canonical executor seam, its implementations, th | [`tool-pwsh/`](tool-pwsh/README.md) | Exposes PowerShell execution to the model. | (registers on `ctx.tools`) | A leaf `cordis.yml` selects one executor implementation and the model-facing tools it needs. A sandboxed composition also selects a `ctx.sandbox` provider; the [ACP example](../../examples/acp-agent/) shows one complete wiring. + +The subsystem reference — request/spec vocabulary, results, background processes, the service, and events — is [docs/subsystems/bash.md](../../docs/subsystems/bash.md). diff --git a/packages/bash/README.zh.md b/packages/bash/README.zh.md index 8b96c4f80b..1d5825e077 100644 --- a/packages/bash/README.zh.md +++ b/packages/bash/README.zh.md @@ -15,3 +15,5 @@ | [`tool-pwsh/`](tool-pwsh/README.md) | 向模型公开 PowerShell 执行。 | (注册到 `ctx.tools`) | 叶节点 `cordis.yml` 选择一个执行器实现和所需的面向模型工具。沙箱化组合还会选择一个 `ctx.sandbox` 提供方;[ACP(Agent Client Protocol)示例](../../examples/acp-agent/)展示一套完整接线。 + +子系统参考——请求/spec 词汇、结果、后台进程、服务与事件——见 [docs/subsystems/bash.md](../../docs/subsystems/bash.md)。 diff --git a/packages/bash/bash-env/tsconfig.json b/packages/bash/bash-env/tsconfig.json index bcf5eb5229..d48ddfc73b 100644 --- a/packages/bash/bash-env/tsconfig.json +++ b/packages/bash/bash-env/tsconfig.json @@ -27,7 +27,7 @@ "path": "../../core/tools" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { "path": "../../support/invariants" diff --git a/packages/bash/bash/README.i18n.yaml b/packages/bash/bash/README.i18n.yaml index 978de64ed1..eee269c121 100644 --- a/packages/bash/bash/README.i18n.yaml +++ b/packages/bash/bash/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/bash/bash/README.md -README.md: 88f519a21a0889d6b7649502c51077940c23709f -README.zh.md: 294044692133da8baa57583146352e84c1ff9946 +README.md: 690f4e61740faf2648ecbc7f5995ec0fdaa64aee +README.zh.md: c343e17bfd97eaf17316c034c263b0c71bb8327a diff --git a/packages/bash/bash/README.md b/packages/bash/bash/README.md index 88f519a21a..690f4e6174 100644 --- a/packages/bash/bash/README.md +++ b/packages/bash/bash/README.md @@ -31,7 +31,7 @@ Implementations subclass `BashExecutor` and implement the abstract methods. Disp `BashExecRequest` (command, workdir?, timeoutMs?, stdoutMaxBytes?, signal?, stdin?, env?, dshEnv?, sandboxPolicy?) resolves to `BashExecSpec` (command, workdir, timeoutMs, stdoutMaxBytes, signal?, stdin?, env?, dshEnv?, sandboxPolicy) before execution. `stdoutMaxBytes` is a trusted foreground-run capture budget for consumers that must parse complete bounded stdout; the model-facing bash tool does not expose it. `sandboxPolicy` is optional on the request and required-but-nullable on the resolved spec: it carries the complete per-call mode and workspace root. The sandbox tool path resolves it from the calling session through `ctx.sandboxPolicy`; a direct sandbox-executor caller falls back to deployment policy, while a non-sandboxing executor carries the field and confines nothing. -The per-session sandbox-mode override vocabulary (the `'sandbox/mode'` event, the `effectiveSandboxMode(events)` fold, and the `setSandboxMode(session, mode)` write path) is NOT here — it is policy state shared by every enforcing family, owned by [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/). `run()` returns `BashRunResult`; `start()` returns `BashProcess`, whose incremental read and kill methods are adapted by `dsh-tool-bash` into a generic task registration. A sandboxing executor stamps `BashSandboxInfo` on foreground results and settled process handles. See `src/types.ts` and [core-data-structures/bash.md](../../../docs/core-data-structures/bash.md). +The per-session sandbox-mode override vocabulary (the `'sandbox/mode'` event, the `effectiveSandboxMode(events)` fold, and the `setSandboxMode(session, mode)` write path) is NOT here — it is policy state shared by every enforcing family, owned by [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/). `run()` returns `BashRunResult`; `start()` returns `BashProcess`, whose incremental read and kill methods are adapted by `dsh-tool-bash` into a generic task registration. A sandboxing executor stamps `BashSandboxInfo` on foreground results and settled process handles. See `src/types.ts` and [subsystems/bash.md](../../../docs/subsystems/bash.md). `stdin` and ordinary `env` are set by in-process plugins (the hooks bridges, native plugins) to feed a hook command its JSON payload and `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` values. `dshEnv` is a separate trusted overlay restricted by type to managed keys; the exported `DSH_ENV_PREFIX` is the single source for that namespace, its `DshEnvironmentKey` template type, executor scrubbing, registry validation, derived built-in names, and model guidance. Model bash uses the current snapshot collected by `ctx.bashEnv`. Implementations remove inherited managed keys, then merge `dshEnv` after ordinary `env`, so an omitted current fact cannot fall back to stale ambient state and an `env` entry cannot displace a managed value. The model-facing tool exposes none of these as parameters. All three remain optional on the resolved spec; absent means no input/overlay. See [the bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [the session environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md). diff --git a/packages/bash/bash/README.zh.md b/packages/bash/bash/README.zh.md index 2940446921..c343e17bfd 100644 --- a/packages/bash/bash/README.zh.md +++ b/packages/bash/bash/README.zh.md @@ -31,7 +31,7 @@ `BashExecRequest`(command、workdir?、timeoutMs?、stdoutMaxBytes?、signal?、stdin?、env?、dshEnv?、sandboxPolicy?)在执行前解析为 `BashExecSpec`(command、workdir、timeoutMs、stdoutMaxBytes、signal?、stdin?、env?、dshEnv?、sandboxPolicy)。`stdoutMaxBytes` 是受信任前台运行的捕获预算,用于必须解析完整有界 stdout 的消费方;面向模型的 bash 工具不公开该字段。`sandboxPolicy` 在请求上可选,在已解析 spec 上必填但可为 null:它携带完整的每次调用模式与工作区根目录。沙箱工具路径通过 `ctx.sandboxPolicy` 从调用会话解析它;沙箱执行器的直接调用方回退到部署策略,非沙箱执行器则携带该字段但不作限制。 -每会话沙箱模式覆盖词汇(`'sandbox/mode'` 事件、`effectiveSandboxMode(events)` fold 以及 `setSandboxMode(session, mode)` 写入路径)不位于此处。它是所有强制执行家族共享的策略状态,属于 [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/)。`run()` 返回 `BashRunResult`;`start()` 返回 `BashProcess`,其增量读取与终止方法由 `dsh-tool-bash` 适配为通用任务注册。沙箱执行器会在前台结果与已结算进程句柄上标记 `BashSandboxInfo`。详见 `src/types.ts` 与 [core-data-structures/bash.md](../../../docs/core-data-structures/bash.md)。 +每会话沙箱模式覆盖词汇(`'sandbox/mode'` 事件、`effectiveSandboxMode(events)` fold 以及 `setSandboxMode(session, mode)` 写入路径)不位于此处。它是所有强制执行家族共享的策略状态,属于 [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/)。`run()` 返回 `BashRunResult`;`start()` 返回 `BashProcess`,其增量读取与终止方法由 `dsh-tool-bash` 适配为通用任务注册。沙箱执行器会在前台结果与已结算进程句柄上标记 `BashSandboxInfo`。详见 `src/types.ts` 与 [subsystems/bash.md](../../../docs/subsystems/bash.md)。 `stdin` 与普通 `env` 由同进程插件(hooks 桥接、原生插件)设置,用于向 hook 命令提供其 JSON payload 和 `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` 值。`dshEnv` 是受类型限制、仅允许受管 key 的独立受信任 overlay;导出的 `DSH_ENV_PREFIX` 是该 namespace、其 `DshEnvironmentKey` 模板类型、执行器清理、注册表验证、派生内置名称与模型指引的统一来源。模型 bash 使用 `ctx.bashEnv` 收集的当前快照。实现会移除继承的受管 key,再在普通 `env` 之后合并 `dshEnv`,因此省略的当前事实不会回退到陈旧环境状态,`env` 条目也无法顶掉受管值。面向模型的工具不将这三者中的任何一个公开为参数。这三者在已解析 spec 上仍然可选;缺失表示没有输入/overlay。详见 [bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) 与 [会话环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。 diff --git a/packages/bash/tool-bash/tsconfig.json b/packages/bash/tool-bash/tsconfig.json index b122ed58ca..93cbe93786 100644 --- a/packages/bash/tool-bash/tsconfig.json +++ b/packages/bash/tool-bash/tsconfig.json @@ -39,7 +39,7 @@ "path": "../../bash/bash-env" }, { - "path": "../../ui/user-approval" + "path": "../../interaction/user-approval" }, { "path": "../../sandbox/sandbox" diff --git a/packages/timeout/README.i18n.yaml b/packages/boot/README.i18n.yaml similarity index 57% rename from packages/timeout/README.i18n.yaml rename to packages/boot/README.i18n.yaml index 33a7a5b26d..8f691dd089 100644 --- a/packages/timeout/README.i18n.yaml +++ b/packages/boot/README.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 packages/timeout/README.md -README.md: d9d8e8a829d8d466d1cc3e42a6c32ac4b3db428b -README.zh.md: c5920258ade7022fb5b28a351ba060eb56e8bc3c +# pnpm run verify-translation-pairing --write packages/boot/README.md +README.md: 5e4e483b60adab0b22ddb5279f4cd8fb699b9c35 +README.zh.md: d49afa45b9fb93f6fa83944167057f0c933cd05d diff --git a/packages/boot/README.md b/packages/boot/README.md new file mode 100644 index 0000000000..5e4e483b60 --- /dev/null +++ b/packages/boot/README.md @@ -0,0 +1,11 @@ +# boot/ — shared app-bin boot glue + +English | [中文](README.zh.md) + +The channel-neutral boot library the app bins share: `apps/cli`, the [`scaffold/`](../scaffold/README.md) launcher, and the [`examples/`](../examples/README.md) demo bins all consume it. + +| Package | Role | ctx key | +|---|---|---| +| `app-boot/` | Shared boot glue for the app bins: `.env` loading, fail-loud Loader guards, snapshot-aware config resolution, the settle-the-tree boot sequence | (library for the bins) | + +The boot sequence and personal-config contract are documented in [`app-boot/README.md`](app-boot/README.md). diff --git a/packages/boot/README.zh.md b/packages/boot/README.zh.md new file mode 100644 index 0000000000..d49afa45b9 --- /dev/null +++ b/packages/boot/README.zh.md @@ -0,0 +1,11 @@ +# boot/:共享的 app bin 启动粘合层 + +[English](README.md) | 中文 + +各 app bin 共享的、与渠道无关的启动库:`apps/cli`、[`scaffold/`](../scaffold/README.md) 启动器与 [`examples/`](../examples/README.md) demo bin 都消费它。 + +| 包 | 职责 | ctx 键 | +|---|---|---| +| `app-boot/` | app bin 的共享启动粘合层:加载 `.env`、会明确报错的 Loader 保护机制、感知快照的配置解析,以及等待整棵树停稳的启动序列 | (供各 bin 使用的库) | + +启动序列与个人配置契约见 [`app-boot/README.md`](app-boot/README.md)。 diff --git a/packages/ui/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml similarity index 81% rename from packages/ui/app-boot/README.i18n.yaml rename to packages/boot/app-boot/README.i18n.yaml index c3193ede46..e6eb080acd 100644 --- a/packages/ui/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.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 packages/ui/app-boot/README.md +# pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md README.md: b3b6cb4e552e8f0a10b496710580805f861847d8 README.zh.md: e73e54f410c1a3e6d592c88ea8214051f83acae0 diff --git a/packages/ui/app-boot/README.md b/packages/boot/app-boot/README.md similarity index 100% rename from packages/ui/app-boot/README.md rename to packages/boot/app-boot/README.md diff --git a/packages/ui/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md similarity index 100% rename from packages/ui/app-boot/README.zh.md rename to packages/boot/app-boot/README.zh.md diff --git a/packages/ui/app-boot/package.json b/packages/boot/app-boot/package.json similarity index 100% rename from packages/ui/app-boot/package.json rename to packages/boot/app-boot/package.json diff --git a/packages/ui/app-boot/src/index.ts b/packages/boot/app-boot/src/index.ts similarity index 99% rename from packages/ui/app-boot/src/index.ts rename to packages/boot/app-boot/src/index.ts index e2d2f78ea1..ac000b985d 100644 --- a/packages/ui/app-boot/src/index.ts +++ b/packages/boot/app-boot/src/index.ts @@ -648,7 +648,7 @@ export function assertEntriesLoaded(ctx: Context, binName: string): void { /** * Value mirrors used because Cordis's const enum has no runtime object to import. - * Keep aligned with `packages/cordis/tool-cordis/src/fiber-state.ts` and + * Keep aligned with `packages/self-modification/tool-cordis/src/fiber-state.ts` and * `packages/client/web/src/loader-status.ts`. */ const FIBER_PENDING = 0 as FiberState.PENDING diff --git a/packages/ui/app-boot/src/invariant.ts b/packages/boot/app-boot/src/invariant.ts similarity index 100% rename from packages/ui/app-boot/src/invariant.ts rename to packages/boot/app-boot/src/invariant.ts diff --git a/packages/ui/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts similarity index 100% rename from packages/ui/app-boot/src/profile.ts rename to packages/boot/app-boot/src/profile.ts diff --git a/packages/ui/app-boot/tests/app-boot.spec.ts b/packages/boot/app-boot/tests/app-boot.spec.ts similarity index 100% rename from packages/ui/app-boot/tests/app-boot.spec.ts rename to packages/boot/app-boot/tests/app-boot.spec.ts diff --git a/packages/ui/app-boot/tests/config-dump.spec.ts b/packages/boot/app-boot/tests/config-dump.spec.ts similarity index 100% rename from packages/ui/app-boot/tests/config-dump.spec.ts rename to packages/boot/app-boot/tests/config-dump.spec.ts diff --git a/packages/ui/app-boot/tests/config-reload.spec.ts b/packages/boot/app-boot/tests/config-reload.spec.ts similarity index 100% rename from packages/ui/app-boot/tests/config-reload.spec.ts rename to packages/boot/app-boot/tests/config-reload.spec.ts diff --git a/packages/ui/app-boot/tests/hmr-config.spec.ts b/packages/boot/app-boot/tests/hmr-config.spec.ts similarity index 100% rename from packages/ui/app-boot/tests/hmr-config.spec.ts rename to packages/boot/app-boot/tests/hmr-config.spec.ts diff --git a/packages/ui/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts similarity index 100% rename from packages/ui/app-boot/tests/profile.spec.ts rename to packages/boot/app-boot/tests/profile.spec.ts diff --git a/packages/ui/app-boot/tests/repository-cache.spec.ts b/packages/boot/app-boot/tests/repository-cache.spec.ts similarity index 100% rename from packages/ui/app-boot/tests/repository-cache.spec.ts rename to packages/boot/app-boot/tests/repository-cache.spec.ts diff --git a/packages/ui/app-boot/tests/user-patches.spec.ts b/packages/boot/app-boot/tests/user-patches.spec.ts similarity index 100% rename from packages/ui/app-boot/tests/user-patches.spec.ts rename to packages/boot/app-boot/tests/user-patches.spec.ts diff --git a/packages/ui/app-boot/tsconfig.json b/packages/boot/app-boot/tsconfig.json similarity index 100% rename from packages/ui/app-boot/tsconfig.json rename to packages/boot/app-boot/tsconfig.json diff --git a/packages/ui/app-boot/tsdown.config.ts b/packages/boot/app-boot/tsdown.config.ts similarity index 100% rename from packages/ui/app-boot/tsdown.config.ts rename to packages/boot/app-boot/tsdown.config.ts diff --git a/packages/bundle/README.i18n.yaml b/packages/bundle/README.i18n.yaml index 27e50409cd..98df59d988 100644 --- a/packages/bundle/README.i18n.yaml +++ b/packages/bundle/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/bundle/README.md -README.md: 4759170435a80e85731446cef21d24fff2abed66 -README.zh.md: 1ef610a1b7b3c591c9a900e04f2d8096b0b086b9 +README.md: af06fe921a66378894a4ab0174ef403499f5f68c +README.zh.md: ef2027e347b79e14574ba1164aa9fddd364327e9 diff --git a/packages/bundle/README.md b/packages/bundle/README.md index 4759170435..af06fe921a 100644 --- a/packages/bundle/README.md +++ b/packages/bundle/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Profile bundles: npm packages whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`, making them installable patch layers for `dsh --profile` compositions ([profile contract](../ui/app-boot/README.md#profiles)). A bundle's substance is its patch list; some also ship runtime glue plugins their patch mounts. +Profile bundles: npm packages whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`, making them installable patch layers for `dsh --profile` compositions ([profile contract](../boot/app-boot/README.md#profiles)). A bundle's substance is its patch list; some also ship runtime glue plugins their patch mounts. | Package | Role | ctx key | |---|---|---| diff --git a/packages/bundle/README.zh.md b/packages/bundle/README.zh.md index 1ef610a1b7..ef2027e347 100644 --- a/packages/bundle/README.zh.md +++ b/packages/bundle/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -Profile 组合包:在 manifest(元数据清单)中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包,因此可作为 patch 层安装进 `dsh --profile` 组合([profile 契约](../ui/app-boot/README.md#profiles))。组合包的实体是它的 patch 列表;有些组合包还附带由其 patch 挂载的运行时粘合插件。 +Profile 组合包:在 manifest(元数据清单)中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包,因此可作为 patch 层安装进 `dsh --profile` 组合([profile 契约](../boot/app-boot/README.md#profiles))。组合包的实体是它的 patch 列表;有些组合包还附带由其 patch 挂载的运行时粘合插件。 | 包 | 职责 | ctx key | |---|---|---| diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index e65d5246a4..2ce3e44fa7 100644 --- a/packages/client/README.i18n.yaml +++ b/packages/client/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/client/README.md -README.md: b6fa426fbe541e2b22d2bf5f19d4397361cf0899 -README.zh.md: 5a55bb8c2c31b5215fc73e75e1c4f3aca79add64 +README.md: 56b9363cc724515ecbd11127ea4c13aba84283df +README.zh.md: edd378bf80b63e9ab2481ab555320ed18de85139 diff --git a/packages/client/README.md b/packages/client/README.md index b6fa426fbe..56b9363cc7 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -38,3 +38,5 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha | [`ui-models/`](ui-models/README.md) | Provides model-provider configuration and DeepSeek onboarding. | Each child reference owns its contract and detailed behavior. The [slot system standard](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) and [web client architecture note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) own the cross-package composition and loading decisions. + +The subsystem reference is [client-modules.md](../../docs/subsystems/client-modules.md); the [slot system standard](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) is the definitive slot model, and the [web client architecture note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) owns the loading chain and object layer. diff --git a/packages/client/README.zh.md b/packages/client/README.zh.md index 5a55bb8c2c..edd378bf80 100644 --- a/packages/client/README.zh.md +++ b/packages/client/README.zh.md @@ -38,3 +38,5 @@ dsh web GUI 的浏览器侧:shell 启动、浏览器与宿主通信、共享 U | [`ui-models/`](ui-models/README.md) | 提供模型提供方配置与 DeepSeek 配置引导。 | 每个子文档负责自身的契约和详细行为。[slot 系统标准](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md)与 [Web 客户端架构 Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md)负责跨包组合与加载决策。 + +子系统参考是 [client-modules.md](../../docs/subsystems/client-modules.md);[slot 系统标准](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md)是权威 slot 模型,[web 客户端架构说明](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md)拥有加载链与对象层。 diff --git a/packages/client/connection/tsconfig.json b/packages/client/connection/tsconfig.json index 6ff7fbfb25..58c04d18bd 100644 --- a/packages/client/connection/tsconfig.json +++ b/packages/client/connection/tsconfig.json @@ -16,7 +16,7 @@ "path": "../../core/session" }, { - "path": "../../ui/commands" + "path": "../../interaction/commands" }, { "path": "../../util/brand" @@ -28,10 +28,10 @@ "path": "../../host/webserver" }, { - "path": "../../ui/user-approval" + "path": "../../interaction/user-approval" }, { - "path": "../../ui/user-interaction" + "path": "../../interaction/user-interaction" }, { "path": "../../support/invariants" diff --git a/packages/client/runtime/tsconfig.json b/packages/client/runtime/tsconfig.json index f93546e855..867b2f588c 100644 --- a/packages/client/runtime/tsconfig.json +++ b/packages/client/runtime/tsconfig.json @@ -24,16 +24,16 @@ "path": "../../host/apiproxy" }, { - "path": "../../ui/commands" + "path": "../../interaction/commands" }, { "path": "../../compact/compact" }, { - "path": "../../session-projection/session-projection" + "path": "../../session/session-projection" }, { - "path": "../../session-title/session-title" + "path": "../../session/session-title" }, { "path": "../../llm/llm" diff --git a/packages/client/ui-conversation/tsconfig.json b/packages/client/ui-conversation/tsconfig.json index 04b265bdd5..89d0067caf 100644 --- a/packages/client/ui-conversation/tsconfig.json +++ b/packages/client/ui-conversation/tsconfig.json @@ -24,7 +24,7 @@ "path": "../runtime" }, { - "path": "../../session-projection/session-projection" + "path": "../../session/session-projection" }, { "path": "../../llm/token-meter" @@ -51,7 +51,7 @@ "path": "../../support/invariants" }, { - "path": "../../ui/permission" + "path": "../../interaction/permission" } ], "exclude": [ diff --git a/packages/client/ui-permission/tsconfig.json b/packages/client/ui-permission/tsconfig.json index 32f84d6a2d..bfc71e2e3c 100644 --- a/packages/client/ui-permission/tsconfig.json +++ b/packages/client/ui-permission/tsconfig.json @@ -39,7 +39,7 @@ "path": "../web-react" }, { - "path": "../../ui/permission" + "path": "../../interaction/permission" }, { "path": "../../support/invariants" diff --git a/packages/client/ui-question/tsconfig.json b/packages/client/ui-question/tsconfig.json index 72d4e96b0f..a6400f7c84 100644 --- a/packages/client/ui-question/tsconfig.json +++ b/packages/client/ui-question/tsconfig.json @@ -29,7 +29,6 @@ { "path": "../ui-slots" }, - { "path": "../../support/invariants" } diff --git a/packages/code-runtime/README.i18n.yaml b/packages/code-runtime/README.i18n.yaml index 3eb462d513..1f9b94c59b 100644 --- a/packages/code-runtime/README.i18n.yaml +++ b/packages/code-runtime/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/code-runtime/README.md -README.md: f20a287419b94b1a9dc1d8da7303fc4d3032cfd3 -README.zh.md: f5cd4c9949f2bd7a7d6d7cd078144910712a3819 +README.md: 45dd55b8609fd33b7116f6b2ea36df3b3332e419 +README.zh.md: ebf288a49f87ad01b1ec80c3843c87c91206af26 diff --git a/packages/code-runtime/README.md b/packages/code-runtime/README.md index f20a287419..45dd55b860 100644 --- a/packages/code-runtime/README.md +++ b/packages/code-runtime/README.md @@ -10,3 +10,5 @@ The code-execution capability seam (see [capability seams](../../.agents/notes/i | [`code-runtime-worker/`](code-runtime-worker/README.md) | Worker-thread backend | registers `ctx.codeRuntime` | Backends register the seam without changing its consumer. The child READMEs own language, isolation, and execution-budget details. + +The subsystem reference — run requests/results, binding namespaces, the failure taxonomy — is [docs/subsystems/code-runtime.md](../../docs/subsystems/code-runtime.md). diff --git a/packages/code-runtime/README.zh.md b/packages/code-runtime/README.zh.md index f5cd4c9949..ebf288a49f 100644 --- a/packages/code-runtime/README.zh.md +++ b/packages/code-runtime/README.zh.md @@ -10,3 +10,5 @@ | [`code-runtime-worker/`](code-runtime-worker/README.md) | Worker 线程后端 | 注册 `ctx.codeRuntime` | 后端在不改变消费方的情况下注册该 seam。子 README 负责语言、隔离和执行预算细节。 + +子系统参考——运行请求/结果、绑定命名空间、失败分类体系——见 [docs/subsystems/code-runtime.md](../../docs/subsystems/code-runtime.md)。 diff --git a/packages/compact/README.i18n.yaml b/packages/compact/README.i18n.yaml index 17357bea7b..bb5922ee75 100644 --- a/packages/compact/README.i18n.yaml +++ b/packages/compact/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/compact/README.md -README.md: 509ea2764250f42e492f787deba959a8dd9967b7 -README.zh.md: adc52690dbe9e904d8f24199cb78493dab3333a4 +README.md: 0a83b87f57e4c948ee6df9e622f17964aff00f0f +README.zh.md: cba53ee7bb917a981a0fd4b77d833b8fb7dd2c3f diff --git a/packages/compact/README.md b/packages/compact/README.md index 509ea27642..0a83b87f57 100644 --- a/packages/compact/README.md +++ b/packages/compact/README.md @@ -12,3 +12,5 @@ A compaction capability family (see [capability seams](../../.agents/notes/imple | [`command-compact/`](command-compact/README.md) | Human compaction command | registers on `ctx.commands` | The backend, optional pruner, and human command compose through the seam; token measurement remains a separate LLM-family service. The [compaction capability-seam Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) owns the dependency rationale. + +The subsystem reference — the `compact/*` events, `CompactionResult`, the service, pruning outcomes — is [docs/subsystems/compaction.md](../../docs/subsystems/compaction.md); the seam's deliberate `dsh-session`/`dsh-llm` dependency is recorded in the [compaction capability-seam Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md). diff --git a/packages/compact/README.zh.md b/packages/compact/README.zh.md index adc52690db..cba53ee7bb 100644 --- a/packages/compact/README.zh.md +++ b/packages/compact/README.zh.md @@ -12,3 +12,5 @@ | [`command-compact/`](command-compact/README.md) | 用户压缩命令 | 注册到 `ctx.commands` | 后端、可选修剪器和用户命令通过该 seam 组合;token 测量仍是独立的 LLM(大语言模型)家族服务。[压缩能力 seam Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)负责依赖关系的设计原理。 + +子系统参考——`compact/*` 事件、`CompactionResult`、服务、剪除结果——见 [docs/subsystems/compaction.md](../../docs/subsystems/compaction.md);seam 有意依赖 `dsh-session`/`dsh-llm` 的决定记录在[压缩能力 seam Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)。 diff --git a/packages/compact/command-compact/README.i18n.yaml b/packages/compact/command-compact/README.i18n.yaml index 35ebe66e2b..68dfebd1b9 100644 --- a/packages/compact/command-compact/README.i18n.yaml +++ b/packages/compact/command-compact/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/compact/command-compact/README.md -README.md: 54f341e39447a423964b7d7435cfb638857eda6e -README.zh.md: d4a122b8a19cdf907212ad019b2528ae52d03886 +README.md: 5af39e82928d64c11474199b6b30058e9d289f45 +README.zh.md: a582bd5272b2e39ac1ffbf839888a4006b6d44bc diff --git a/packages/compact/command-compact/README.md b/packages/compact/command-compact/README.md index 54f341e394..5af39e8292 100644 --- a/packages/compact/command-compact/README.md +++ b/packages/compact/command-compact/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Human-facing `/compact` control over [`ctx.compact`](../compact/README.md). The plugin registers one global command through [`ctx.commands`](../../ui/commands/README.md), so every composed command adapter discovers and executes it without a model turn. The [queued manual compaction Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md) owns the admission, lock, and durability decisions. +Human-facing `/compact` control over [`ctx.compact`](../compact/README.md). The plugin registers one global command through [`ctx.commands`](../../interaction/commands/README.md), so every composed command adapter discovers and executes it without a model turn. The [queued manual compaction Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md) owns the admission, lock, and durability decisions. ## Command contract diff --git a/packages/compact/command-compact/README.zh.md b/packages/compact/command-compact/README.zh.md index d4a122b8a1..a582bd5272 100644 --- a/packages/compact/command-compact/README.zh.md +++ b/packages/compact/command-compact/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -通过 [`ctx.compact`](../compact/README.md) 提供面向用户的 `/compact` 压缩(compaction)控制。该插件通过 [`ctx.commands`](../../ui/commands/README.md) 注册一个全局命令,因此组合中的每个命令适配器都能发现并执行它,无需模型轮次。[排队手动压缩 Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md)拥有接纳、锁与持久性决策。 +通过 [`ctx.compact`](../compact/README.md) 提供面向用户的 `/compact` 压缩(compaction)控制。该插件通过 [`ctx.commands`](../../interaction/commands/README.md) 注册一个全局命令,因此组合中的每个命令适配器都能发现并执行它,无需模型轮次。[排队手动压缩 Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md)拥有接纳、锁与持久性决策。 ## 命令契约 diff --git a/packages/compact/command-compact/tsconfig.json b/packages/compact/command-compact/tsconfig.json index f99f2b98b4..c1040bf92a 100644 --- a/packages/compact/command-compact/tsconfig.json +++ b/packages/compact/command-compact/tsconfig.json @@ -15,7 +15,7 @@ "path": "../../../vendor/cordis" }, { - "path": "../../ui/commands" + "path": "../../interaction/commands" }, { "path": "../compact" diff --git a/packages/compact/compact/README.i18n.yaml b/packages/compact/compact/README.i18n.yaml index 6463edbc33..1252d99e0f 100644 --- a/packages/compact/compact/README.i18n.yaml +++ b/packages/compact/compact/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/compact/compact/README.md -README.md: cd2404cda8d8702ca0400d3f23d9e1fe041495a1 -README.zh.md: ed6ccc630118dc1bd6d6ea761beefafadb4bd48f +README.md: 2bf39cac91798b1e0fdb679d2a9b674caceffe22 +README.zh.md: 330b5b0cb9d5c2bf3953354112f3488447e71f87 diff --git a/packages/compact/compact/README.md b/packages/compact/compact/README.md index cd2404cda8..2bf39cac91 100644 --- a/packages/compact/compact/README.md +++ b/packages/compact/compact/README.md @@ -24,7 +24,7 @@ All three operations are **abstract** — the backend owns trigger policy, reten | `compactNow(agent, signal)` | Explicitly compact one useful balanced older span even below automatic pressure. It synchronously reserves idle turn admission before yielding, writes nothing when no useful span exists, records a standalone `compact/* { turn: null }` attempt before summarization, and awaits its durability checkpoint before release. Expected operational failures use `ManualCompactionError`; cancellation rethrows the exact abort reason. | | `compactRegion(start, end, agent, signal?)` | Forcibly summarize surface nodes `[start, end]` (inclusive seqs) from `agent.session` into a single replacement node whose source is `COMPACT_CHECKPOINT_SOURCE`. **Throws** if a compaction is already in progress, if `start`/`end` aren't surface nodes, or if `start` is positioned after `end` on the surface. The range is a SURFACE-POSITION span, not a numeric seq interval — after a prior replace lands a fresh high-seq summary node at the shadowed range's position, surface order no longer tracks seq order. | -`CompactionResult` keeps the raw summary and bookkeeping-event seqs available to callers alongside the shadowed range and token accounting; its drift-checked shape lives in the [compaction data-structure reference](../../../docs/core-data-structures/compaction.md#compactionresult). +`CompactionResult` keeps the raw summary and bookkeeping-event seqs available to callers alongside the shadowed range and token accounting; its drift-checked shape lives in the [compaction data-structure reference](../../../docs/subsystems/compaction.md#compactionresult). `compactIfNeeded` and `compactNow` take a required `signal`; `compactRegion`'s is optional. A backend that summarizes via `ctx.llm.stream()` **must** forward it into the call's `GenerateOptions.signal`, so an abort or fiber dispose tears down the in-flight summarization. Automatic and explicit-region brackets recover their numeric owner from the currently open turn. Manual brackets require no open turn and stamp `turn: null`. @@ -68,7 +68,7 @@ Subclass `CompactService`, implement `compactIfNeeded`, `compactNow`, and `compa ## Recognizing a checkpoint outside the host program (`./checkpoint`) -`COMPACT_CHECKPOINT_SOURCE` and `isCompactCheckpointSource()` are declared on the `@deepseek-ai/dsh-compact/checkpoint` subpath and re-exported from the root, so host-side consumers keep reading them from the root. The leaf imports no cordis and declares no module augmentation (the [`dsh-commands/brand`](../../ui/commands/README.md) shape), which is what lets a client or wire program name the checkpoint source: the package **root** cannot enter such a program at all, because it reaches `dsh-session`'s root and that `Context` merge declares the host `sessions` service against the client's own (`TS2717` — one program per side, per [development.md](../../../docs/development.md#typescript-project-layout)). The web client's transcript adapter pins its plugin literal to this leaf with a type-only import, so renaming the plugin id here is a compile error there. +`COMPACT_CHECKPOINT_SOURCE` and `isCompactCheckpointSource()` are declared on the `@deepseek-ai/dsh-compact/checkpoint` subpath and re-exported from the root, so host-side consumers keep reading them from the root. The leaf imports no cordis and declares no module augmentation (the [`dsh-commands/brand`](../../interaction/commands/README.md) shape), which is what lets a client or wire program name the checkpoint source: the package **root** cannot enter such a program at all, because it reaches `dsh-session`'s root and that `Context` merge declares the host `sessions` service against the client's own (`TS2717` — one program per side, per [development.md](../../../docs/development.md#typescript-project-layout)). The web client's transcript adapter pins its plugin literal to this leaf with a type-only import, so renaming the plugin id here is a compile error there. ## Model Experience diff --git a/packages/compact/compact/README.zh.md b/packages/compact/compact/README.zh.md index ed6ccc6301..330b5b0cb9 100644 --- a/packages/compact/compact/README.zh.md +++ b/packages/compact/compact/README.zh.md @@ -24,7 +24,7 @@ | `compactNow(agent, signal)` | 即使未达到自动压力,也显式压缩一段有效、平衡的较早范围。该操作会在让出控制权前同步预留空闲轮次接纳;没有有效范围时不写入任何内容;在摘要前记录独立的 `compact/* { turn: null }` 尝试;释放预留前等待其持久性检查点。预期操作失败使用 `ManualCompactionError`;取消会原样重新抛出 abort 原因。 | | `compactRegion(start, end, agent, signal?)` | 强制将表层节点 `[start, end]`(包含两端 seq)从 `agent.session` 摘要为单个替换节点,其源为 `COMPACT_CHECKPOINT_SOURCE`。如果压缩已在进行、`start`/`end` 不是表层节点,或 `start` 在表层上位于 `end` 之后,则**抛出异常**。该范围是表层位置范围,不是数值 seq 区间:在之前的 replace 将新生成的高 seq 摘要节点放到已遮蔽范围的位置之后,表层顺序不再跟随 seq 顺序。 | -`CompactionResult` 向调用方保留原始摘要与记录操作过程的事件 seq,同时保留已遮蔽范围与 token 计量;其结构由漂移检查保障,定义见 [压缩数据结构参考](../../../docs/core-data-structures/compaction.md#compactionresult)。 +`CompactionResult` 向调用方保留原始摘要与记录操作过程的事件 seq,同时保留已遮蔽范围与 token 计量;其结构由漂移检查保障,定义见 [压缩数据结构参考](../../../docs/subsystems/compaction.md#compactionresult)。 `compactIfNeeded` 和 `compactNow` 必须传入 `signal`;`compactRegion` 的该参数可选。通过 `ctx.llm.stream()` 摘要的后端**必须** 将它转发到调用的 `GenerateOptions.signal`,因此 abort 或 fiber dispose(资源释放)会停止进行中的摘要。自动和显式范围标记对会从当前打开的轮次恢复其数字形式归属。手动标记对不要求存在打开的轮次,并标记 `turn: null`。 @@ -68,7 +68,7 @@ ## 在 host 程序之外识别检查点(`./checkpoint`) -`COMPACT_CHECKPOINT_SOURCE` 与 `isCompactCheckpointSource()` 声明在 `@deepseek-ai/dsh-compact/checkpoint` 子路径上,并由包根重新导出,因此 host 侧消费方仍从根读取它们。该叶子不导入 cordis、也不声明任何模块增强(即 [`dsh-commands/brand`](../../ui/commands/README.md) 的形状),这正是客户端或 wire 程序能够命名该检查点来源的原因:包的**根**根本无法进入这类程序,因为它会到达 `dsh-session` 的根,而那处 `Context` 合并会让 host 的 `sessions` 服务与客户端自己的冲突(`TS2717`——每侧一个程序,见 [development.md](../../../docs/development.md#typescript-project-layout))。Web 客户端的对话记录适配器用仅类型导入把它的插件字面量钉在该叶子上,因此在此处改插件 id 会让那边编译失败。 +`COMPACT_CHECKPOINT_SOURCE` 与 `isCompactCheckpointSource()` 声明在 `@deepseek-ai/dsh-compact/checkpoint` 子路径上,并由包根重新导出,因此 host 侧消费方仍从根读取它们。该叶子不导入 cordis、也不声明任何模块增强(即 [`dsh-commands/brand`](../../interaction/commands/README.md) 的形状),这正是客户端或 wire 程序能够命名该检查点来源的原因:包的**根**根本无法进入这类程序,因为它会到达 `dsh-session` 的根,而那处 `Context` 合并会让 host 的 `sessions` 服务与客户端自己的冲突(`TS2717`——每侧一个程序,见 [development.md](../../../docs/development.md#typescript-project-layout))。Web 客户端的对话记录适配器用仅类型导入把它的插件字面量钉在该叶子上,因此在此处改插件 id 会让那边编译失败。 ## 模型体验 diff --git a/packages/context/README.i18n.yaml b/packages/context/README.i18n.yaml index f3acd7f2bf..efb18426a4 100644 --- a/packages/context/README.i18n.yaml +++ b/packages/context/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/context/README.md -README.md: a0b5eef51740b3aa67b9e158a00357f849b8bf57 -README.zh.md: d6ed8050f3a64f740a951883f0dc321ac08e978a +README.md: 237f6191329f89834ec9bd9f72c0a1fd1d94651e +README.zh.md: 7c55dd91dbf7e3fca222e4b6f6bed2769f5c253c diff --git a/packages/context/README.md b/packages/context/README.md index a0b5eef517..237f619132 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -12,3 +12,5 @@ Product plugins that add model-visible request context without defining a tool. | [`workspace-context/`](workspace-context/README.md) | Workspace-instruction context | — | The [`workspace-context` decision record](../../.agents/notes/implemented/feature/2026-06-24-workspace-context.md) explains its per-agent/session isolation and lifecycle split. + +Session references are documented in [docs/subsystems/session-reference.md](../../docs/subsystems/session-reference.md); the [`workspace-context` decision record](../../.agents/notes/implemented/feature/2026-06-24-workspace-context.md) owns its per-agent/session isolation and lifecycle split. diff --git a/packages/context/README.zh.md b/packages/context/README.zh.md index d6ed8050f3..7c55dd91db 100644 --- a/packages/context/README.zh.md +++ b/packages/context/README.zh.md @@ -12,3 +12,5 @@ | [`workspace-context/`](workspace-context/README.md) | workspace 指令上下文 | — | [`workspace-context` 决策记录](../../.agents/notes/implemented/feature/2026-06-24-workspace-context.md)解释其逐 agent(智能体)/会话隔离和生命周期拆分。 + +会话引用见 [docs/subsystems/session-reference.md](../../docs/subsystems/session-reference.md);[`workspace-context` 决策记录](../../.agents/notes/implemented/feature/2026-06-24-workspace-context.md)拥有其按 agent/会话隔离与生命周期拆分。 diff --git a/packages/cordis/README.md b/packages/cordis/README.md deleted file mode 100644 index e8b0790f91..0000000000 --- a/packages/cordis/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# packages/cordis — Cordis runtime integration - -English | [中文](README.zh.md) - -Plugins that integrate Harness-owned formats with the Cordis runtime: the self-referential model toolset and the restricted repository Plugin runtime. - -| Package | Role | ctx key | -|---|---|---| -| [`tool-cordis/`](tool-cordis/README.md) | Model-facing runtime inspection and temporary-plugin tools | registers on `ctx.tools` | -| [`repository-plugin/`](repository-plugin/README.md) | Repository skill and MCP composition | registers a Loader builtin | diff --git a/packages/cordis/README.zh.md b/packages/cordis/README.zh.md deleted file mode 100644 index c452f9a7ef..0000000000 --- a/packages/cordis/README.zh.md +++ /dev/null @@ -1,10 +0,0 @@ -# packages/cordis — Cordis 运行时集成 - -[English](README.md) | 中文 - -把 Harness 所有的格式与 Cordis 运行时集成的插件:自指的面向模型工具集,以及受限的 repository Plugin 运行时。 - -| 包 | 职责 | ctx key | -|---|---|---| -| [`tool-cordis/`](tool-cordis/README.md) | 面向模型的运行时检查和临时插件工具 | 注册到 `ctx.tools` | -| [`repository-plugin/`](repository-plugin/README.md) | repository skill 与 MCP 组合 | 注册一个 Loader builtin | diff --git a/packages/core/README.i18n.yaml b/packages/core/README.i18n.yaml index d43caa011e..aa542fe885 100644 --- a/packages/core/README.i18n.yaml +++ b/packages/core/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/README.md -README.md: d51ef73f7f545920f8cd527b2b0d597305ae86d0 -README.zh.md: 729f41fafb6b9585a66e58fe8d6ac63c1f235acc +README.md: 40a888183a9338805d83c86bab9ae16f1c7bc04f +README.zh.md: 496d547a173a63ac9d8d2ce0f005877dab92d358 diff --git a/packages/core/README.md b/packages/core/README.md index d51ef73f7f..40a888183a 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -16,3 +16,5 @@ The session log, system-prompt assembly, tool registry, agent vocabulary, and co `scope` supplies the shared scoping primitive. `agent` owns the public seam, while `agent-loop` is its default implementation; extension plugins depend on the seam so the driver remains swappable. Runnable compositions belong to [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md); this group owns only the swappable spine pieces. + +The subsystem reference — the package-by-package loop map, the `Agent` handle and its delivery/interception contracts — is [docs/subsystems/core.md](../../docs/subsystems/core.md); the default runnable composition is [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md). diff --git a/packages/core/README.zh.md b/packages/core/README.zh.md index 729f41fafb..496d547a17 100644 --- a/packages/core/README.zh.md +++ b/packages/core/README.zh.md @@ -16,3 +16,5 @@ `scope` 提供共享作用域原语。`agent` 负责公开 seam,`agent-loop` 是其默认实现;扩展插件依赖该 seam,从而保持驱动器可替换。 可运行组合属于 [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md);该分组只负责可替换的主干组件。 + +子系统参考——逐包循环地图、`Agent` 句柄及其投递/拦截契约——见 [docs/subsystems/core.md](../../docs/subsystems/core.md);默认可运行组合是 [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md)。 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-loop/tsconfig.json b/packages/core/agent-loop/tsconfig.json index 0949f18453..c3504e4eb4 100644 --- a/packages/core/agent-loop/tsconfig.json +++ b/packages/core/agent-loop/tsconfig.json @@ -24,7 +24,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { "path": "../../core/system-prompt" 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 0b2d50957e..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: e2c014a1448125b23475d6cdf52c02f10fc54794 -README.zh.md: a4aeead796961bd66a6c7ca1f9a66dffb60eba5d +README.md: 00e007ee6064f485bf80dfda0227ab6e834f6414 +README.zh.md: 5f06fee5d50bc2c9dc333874a8354fbf19d5248b diff --git a/packages/core/session/README.md b/packages/core/session/README.md index e2c014a144..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` @@ -56,7 +56,7 @@ The shared [storage codec](src/chunk-rows.ts) losslessly converts event sequence ### Surface types -This package owns ordered surface projection, replacement validation, replay, and the type guards that distinguish append-origin from replacement events. The [surface type catalog](../../../docs/core-data-structures/session.md#surface-types) owns the exact shapes and field semantics. A human transcript must project append-origin events rather than `session.surface`, because landed replacements shadow history the reader already saw; model-facing consumers continue to read `session.surface`. +This package owns ordered surface projection, replacement validation, replay, and the type guards that distinguish append-origin from replacement events. The [surface type catalog](../../../docs/subsystems/session.md#surface-types) owns the exact shapes and field semantics. A human transcript must project append-origin events rather than `session.surface`, because landed replacements shadow history the reader already saw; model-facing consumers continue to read `session.surface`. ### Request-header reconstruction (`request-header.ts`) diff --git a/packages/core/session/README.zh.md b/packages/core/session/README.zh.md index a4aeead796..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` @@ -56,7 +56,7 @@ ### Surface 类型 -此包拥有有序 surface 投影、替换校验、回放,以及区分追加来源事件与替换事件的类型守卫。[surface 类型目录](../../../docs/core-data-structures/session.md#surface-types)拥有精确形状与字段语义。面向人的 transcript(文本记录)必须投影追加来源事件,而不是 `session.surface`,因为已落地的替换会遮蔽读者已经看到的历史;面向模型的消费方继续读取 `session.surface`。 +此包拥有有序 surface 投影、替换校验、回放,以及区分追加来源事件与替换事件的类型守卫。[surface 类型目录](../../../docs/subsystems/session.md#surface-types)拥有精确形状与字段语义。面向人的 transcript(文本记录)必须投影追加来源事件,而不是 `session.surface`,因为已落地的替换会遮蔽读者已经看到的历史;面向模型的消费方继续读取 `session.surface`。 ### 请求头重建(`request-header.ts`) 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 c1bd91ce2b..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: 81cc57983d83fd19468017b217d4db9978f4e228 -README.zh.md: 9f875bd80a03d1d0f78625ee98eeaad9d118f871 +README.md: f3f25f908a4ba8016d38f7777fe72691dba4afb1 +README.zh.md: 796ee8e1aa8de88f979d825df5e0b4f39ab7bcf1 diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 81cc57983d..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 @@ -45,7 +45,7 @@ The live registry pipeline has three transformable waterfalls, then the definiti - `ToolExecution` — the readonly pipeline view: immutable `{ token, callId, name, arguments, signal, agent?, parent? }`; the registry separately retains and re-fuses the original caller signal. `ToolDispatchExecution` is the `tools/execute`-only view whose required signal is mutable, so a wrapper may replace and restore it but cannot delete it. A nested call's `parent` is a `ToolExecutionToken`, not an execution object. - `ToolRunContext` — the execution passed to a tool body, extending `ToolExecution` with `deferContext(context)`. It defers one context until the tool's final result reaches the loop — typically a nested-dispatch context ferried by a composite tool, or a fresh plugin-sourced instruction minted by a leaf tool (`tool-goal`'s wrap-up) — even when the tool later throws or cancellation wins; it never injects immediately. - `ToolExecutionResult` — discriminated execution-local outcome. Success is `{ isError:false, value:JsonValue, content, meta?, additionalContexts? }`; failure is `{ isError:true, error:{ message, info? }, content, meta?, additionalContexts? }` and has no value. Call identity stays on the immutable `ToolExecution`. The registry snapshots, validates, and freezes the canonical value before rendering, then materializes the durable presentation fields before final observation. `ToolFailure.info` carries an internal `{ name, code }` for a `HarnessError`; `additionalContexts` preserves every deferred or post-execute identified `UserMessage` for the loop's post-result FIFO. -- `PreToolDecision` — `{kind:'allow'}` | `{kind:'deny', reason}` | `{kind:'ask', reason?}`. Input rewrite is deliberately not offered; `ask` is serviced by [`ctx.approval`](../../ui/user-approval/README.md) when mounted and otherwise degrades to deny. +- `PreToolDecision` — `{kind:'allow'}` | `{kind:'deny', reason}` | `{kind:'ask', reason?}`. Input rewrite is deliberately not offered; `ask` is serviced by [`ctx.approval`](../../interaction/user-approval/README.md) when mounted and otherwise degrades to deny. - `PostToolDecision` — accept may replace `content` or `value`, never both, and may attach `additionalContexts`; block turns feedback into a valueless failure. Content replacement preserves the canonical value and metadata. Value replacement is revalidated and rerenders content/metadata. Accept preserves tool-deferred contexts before decision contexts; block discards tool-deferred contexts and exposes only contexts explicitly supplied by the blocking decision. - `ToolGuard` — `(execution) => string | undefined`; the returned string is a final monotonic denial reason evaluated after the reorderable pre-execute waterfall and before dispatch. - `ToolCallView` / `ToolResultView` — provider-neutral `card`-tagged render intents a tool returns from `presentCall` / `presentResult` to own how a UI renders ITS calls (see "Tool-owned UI presentation"). @@ -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 9f875bd80a..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 随后追加的持久会话事件。 ### 关键类型 @@ -45,7 +45,7 @@ tools: - `ToolExecution`:只读流水线视图:不可变的 `{ token, callId, name, arguments, signal, agent?, parent? }`;注册表会另行保留并重新融合调用方的原始信号。`ToolDispatchExecution` 是仅供 `tools/execute` 使用的视图,其必填信号可变,因此包装层可以替换并还原它,但不能删除它。嵌套调用的 `parent` 是 `ToolExecutionToken`,而不是执行对象。 - `ToolRunContext`:传给工具主体的执行上下文,在 `ToolExecution` 基础上增加 `deferContext(context)`。它把一条上下文推迟到该工具的最终结果抵达循环时——通常是组合工具转运的嵌套分发上下文,也可以是叶子工具铸造的全新插件来源指令(如 `tool-goal` 的收尾注入)——即使工具后来抛出或取消胜出也不例外;该方法绝不会立即注入上下文。 - `ToolExecutionResult`:可辨识的执行局部结果。成功形态为 `{ isError:false, value:JsonValue, content, meta?, additionalContexts? }`;失败形态为 `{ isError:true, error:{ message, info? }, content, meta?, additionalContexts? }`,且不含值。调用身份保留在不可变的 `ToolExecution` 上。注册表会在呈现前快照、验证并冻结规范值,随后在最终观测前实体化持久呈现字段。`ToolFailure.info` 携带内部的 `{ name, code }`,用于表示 `HarnessError`;`additionalContexts` 会保留每个通过延迟或 post-execute 加入且带标识的 `UserMessage`,供循环在结果后按 FIFO 顺序处理。 -- `PreToolDecision`:`{kind:'allow'}` | `{kind:'deny', reason}` | `{kind:'ask', reason?}`。该类型有意不提供输入改写;`ask` 在挂载 [`ctx.approval`](../../ui/user-approval/README.md) 时由它处理,否则退化为拒绝。 +- `PreToolDecision`:`{kind:'allow'}` | `{kind:'deny', reason}` | `{kind:'ask', reason?}`。该类型有意不提供输入改写;`ask` 在挂载 [`ctx.approval`](../../interaction/user-approval/README.md) 时由它处理,否则退化为拒绝。 - `PostToolDecision`:接受决定可以替换 `content` 或 `value`(不能同时替换),并可附加 `additionalContexts`;阻止决定会把反馈变成无值失败。替换内容会保留规范值和元数据。替换值会重新验证,并重新呈现内容/元数据。接受决定会先保留工具延迟的上下文,再附加决定上下文;阻止决定会丢弃工具延迟的上下文,只公开阻止决定显式提供的上下文。 - `ToolGuard`:`(execution) => string | undefined`;返回的字符串是最终单调拒绝理由,在可重排的前置执行 waterfall 之后、分发之前求值。 - `ToolCallView` / `ToolResultView`:提供方无关、带 `card` 标签的呈现意图;工具通过 `presentCall` / `presentResult` 返回该意图,从而拥有 UI 呈现其自身调用的方式(参见「工具拥有的 UI 呈现」)。 @@ -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/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 30e139bca8..9677ec7847 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -39,7 +39,7 @@ import { renderToolsSdkPy } from './py-types.ts' * the flavor table is checked against too, so any of the three left out is a * typecheck failure. What no check reaches is the prose that names the values * instead of deriving them: the seam's `dsh-code-runtime` README pair, its - * `CodeRuntime.language` JSDoc, and `docs/core-data-structures/code-runtime.md` + * `CodeRuntime.language` JSDoc, and `docs/subsystems/code-runtime.md` * with its zh pair, plus this package's own README pair and the * {@link Config.mode} JSDoc. */ diff --git a/packages/core/tools/tsconfig.json b/packages/core/tools/tsconfig.json index 918112d7d0..de5756ecbd 100644 --- a/packages/core/tools/tsconfig.json +++ b/packages/core/tools/tsconfig.json @@ -33,7 +33,7 @@ "path": "../../core/scope" }, { - "path": "../../ui/user-approval" + "path": "../../interaction/user-approval" }, { "path": "../../support/invariants" diff --git a/packages/credentials/README.i18n.yaml b/packages/credentials/README.i18n.yaml index 04f14e8e75..38cbbb2968 100644 --- a/packages/credentials/README.i18n.yaml +++ b/packages/credentials/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/credentials/README.md -README.md: c08831c90333f8515bf86a5af717c38c50b50817 -README.zh.md: c9756c010af5d2db0fc41f3108eabdc7f1161293 +README.md: 75e3941ff6421ca7e41a71ab2bfe8b30209ce598 +README.zh.md: d7c958689fdffc82830a82b04e2a06802bc81634 diff --git a/packages/credentials/README.md b/packages/credentials/README.md index c08831c903..75e3941ff6 100644 --- a/packages/credentials/README.md +++ b/packages/credentials/README.md @@ -10,3 +10,5 @@ The credential capability family separates reference resolution from its provide | [`credentials-local/`](credentials-local/README.md) | Environment and local-file provider | registers `ctx.credentials` | Configuration carries references, not secret values. Consumers resolve those references at their operation boundary; the child READMEs own mutation, precedence, and storage semantics. + +The subsystem reference — `CredentialRef`, per-operation resolution, UI-safe `CredentialInfo`, provider layers — is [docs/subsystems/credentials.md](../../docs/subsystems/credentials.md). diff --git a/packages/credentials/README.zh.md b/packages/credentials/README.zh.md index c9756c010a..d7c958689f 100644 --- a/packages/credentials/README.zh.md +++ b/packages/credentials/README.zh.md @@ -10,3 +10,5 @@ | [`credentials-local/`](credentials-local/README.md) | 环境与本地文件提供方 | 注册 `ctx.credentials` | 配置携带引用而非机密值。消费方在其操作边界解析这些引用;变更、优先级与存储语义由子级 README 负责。 + +子系统参考——`CredentialRef`、按操作解析、对 UI 安全的 `CredentialInfo`、提供方层——见 [docs/subsystems/credentials.md](../../docs/subsystems/credentials.md)。 diff --git a/packages/credentials/credentials-local/README.i18n.yaml b/packages/credentials/credentials-local/README.i18n.yaml index 07a3efd5c3..182ac31809 100644 --- a/packages/credentials/credentials-local/README.i18n.yaml +++ b/packages/credentials/credentials-local/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/credentials/credentials-local/README.md -README.md: 8e95a890a8e38172cf8984653a01c59570f0061a -README.zh.md: 04ad07ae4e703ab0416d1d8f1bb6a6ff90adf337 +README.md: 6051c07628c0214f9396d12554719c5614610826 +README.zh.md: c6f3901af925d4bd31d2dcfbf5f5f65de62dac81 diff --git a/packages/credentials/credentials-local/README.md b/packages/credentials/credentials-local/README.md index 8e95a890a8..6051c07628 100644 --- a/packages/credentials/credentials-local/README.md +++ b/packages/credentials/credentials-local/README.md @@ -51,7 +51,7 @@ External edits publish `credentials/updated` per changed reference after the sna ## Security boundary -The document is `0600` under a `0700` directory, which stops other OS users — **not** the model. Tool processes (bash, the filesystem tools) run as the same user, and the shipped `workspace-write` file policy confines mutations rather than reads, so they can read this file exactly like any other file the user owns; no sandbox mode singles it out. What the harness does hold to is narrower: it never hands the model a resolved path to the document, and never loads it into the process environment — unlike `$DSH_HOME/.env`, which is the user's ordinary environment layer (see [app-boot's Harness-home layers](../../ui/app-boot/README.md#profiles)) — so reaching the value takes a deliberate read of a path the agent was not given. +The document is `0600` under a `0700` directory, which stops other OS users — **not** the model. Tool processes (bash, the filesystem tools) run as the same user, and the shipped `workspace-write` file policy confines mutations rather than reads, so they can read this file exactly like any other file the user owns; no sandbox mode singles it out. What the harness does hold to is narrower: it never hands the model a resolved path to the document, and never loads it into the process environment — unlike `$DSH_HOME/.env`, which is the user's ordinary environment layer (see [app-boot's Harness-home layers](../../boot/app-boot/README.md#profiles)) — so reaching the value takes a deliberate read of a path the agent was not given. That is discretion, not a boundary. A deployment that must keep provider keys away from its own agent cannot get there with file permissions; an OS-keychain provider — a store the model's processes cannot read at all — is the deferred answer and belongs beside this provider as a sibling package. diff --git a/packages/credentials/credentials-local/README.zh.md b/packages/credentials/credentials-local/README.zh.md index 04ad07ae4e..c6f3901af9 100644 --- a/packages/credentials/credentials-local/README.zh.md +++ b/packages/credentials/credentials-local/README.zh.md @@ -51,7 +51,7 @@ OPENAI_API_KEY: sk-… ## 安全边界 -文档在 `0700` 目录下以 `0600` 权限存放,这挡得住其他 OS 用户,**挡不住**模型。工具进程(bash、文件系统工具)以同一用户身份运行,而已交付的 `workspace-write` 文件策略限制的是修改而非读取,因此它们读这个文件与读该用户拥有的任何其他文件毫无二致;也没有任何沙箱模式会把它单独挑出来。harness 真正守住的更窄:它绝不把该文档的解析后路径交给模型,也绝不把它载入进程环境——这与用户的普通环境层 `$DSH_HOME/.env` 不同(见 [app-boot 的 Harness home 各层](../../ui/app-boot/README.md#profiles))——因此要拿到这个值,需要刻意去读一条并未交给 agent(智能体)的路径。 +文档在 `0700` 目录下以 `0600` 权限存放,这挡得住其他 OS 用户,**挡不住**模型。工具进程(bash、文件系统工具)以同一用户身份运行,而已交付的 `workspace-write` 文件策略限制的是修改而非读取,因此它们读这个文件与读该用户拥有的任何其他文件毫无二致;也没有任何沙箱模式会把它单独挑出来。harness 真正守住的更窄:它绝不把该文档的解析后路径交给模型,也绝不把它载入进程环境——这与用户的普通环境层 `$DSH_HOME/.env` 不同(见 [app-boot 的 Harness home 各层](../../boot/app-boot/README.md#profiles))——因此要拿到这个值,需要刻意去读一条并未交给 agent(智能体)的路径。 这是审慎,不是边界。必须让提供方密钥远离自身 agent 的部署无法靠文件权限做到;OS 钥匙串提供方——一个模型的进程根本读不到的存储——才是延后的答案,它应当作为平级包与本提供方并列。 diff --git a/packages/e2b/e2b/package.json b/packages/e2b/e2b/package.json index a77e3d9464..5abacb278a 100644 --- a/packages/e2b/e2b/package.json +++ b/packages/e2b/e2b/package.json @@ -35,6 +35,7 @@ "devDependencies": { "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-loader-smoke": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/e2b/e2b/tests/composition.e2e.ts b/packages/e2b/e2b/tests/composition.e2e.ts index 01316aeb2d..6da102a827 100644 --- a/packages/e2b/e2b/tests/composition.e2e.ts +++ b/packages/e2b/e2b/tests/composition.e2e.ts @@ -13,6 +13,7 @@ import { } from '@deepseek-ai/dsh-e2b' import PtyService, { PtySessionId } from '@deepseek-ai/dsh-pty' import { LocalPtyBackend } from '@deepseek-ai/dsh-pty-local' +import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy' import { Session, SessionId } from '@deepseek-ai/dsh-session' import E2BSubprocessService from '@deepseek-ai/dsh-subprocess-e2b' @@ -51,10 +52,10 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => { runtimeRoot: '/home/user/.dsh-e2b', getSandbox: async () => sandbox, } as never) - ctx.provide('sandboxPolicy', { - defaultMode: 'danger-full-access', + const sandboxPolicyFiber = await ctx.plugin(SandboxPolicyService, { + mode: 'danger-full-access', workspaceRoot: '/home/user', - } as never) + }) const ptyFiber = await ctx.plugin(PtyService) const subprocessFiber = await ctx.plugin(E2BSubprocessService) const node = await ctx.subprocess.resolveExecutable('node') @@ -114,6 +115,7 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => { await session.close('environment test complete') await subprocessFiber.dispose() await ptyFiber.dispose() + await sandboxPolicyFiber.dispose() } finally { await sandbox.kill().catch(() => false) diff --git a/packages/examples/acp-demo/tests/built-bin.e2e.ts b/packages/examples/acp-demo/tests/built-bin.e2e.ts index 6f67baa5a5..14b2c3f607 100644 --- a/packages/examples/acp-demo/tests/built-bin.e2e.ts +++ b/packages/examples/acp-demo/tests/built-bin.e2e.ts @@ -33,9 +33,9 @@ const decompress = promisify(zstdDecompress) const dshPackages = [ 'examples/agent-spine-demo', 'core/agent', 'core/session', 'core/system-prompt', 'core/tools', 'core/agent-loop', 'llm/llm', 'bash/bash', - 'bash/bash-local', 'bash/tool-bash', 'subprocess/subprocess', 'subprocess/subprocess-local', 'context/workspace-context', 'support/invariants', 'ui/app-boot', - 'session-persistence/session-persistence', - 'session-persistence/session-checkpoint-policy', 'session-persistence/session-persistence-jsonl', + 'bash/bash-local', 'bash/tool-bash', 'subprocess/subprocess', 'subprocess/subprocess-local', 'context/workspace-context', 'support/invariants', 'boot/app-boot', + 'session/session-persistence', + 'session/session-checkpoint-policy', 'session/session-persistence-jsonl', 'acp/acp', 'examples/acp-demo', 'util/paths', ] const vendorPackages = [ diff --git a/packages/examples/acp-demo/tsconfig.json b/packages/examples/acp-demo/tsconfig.json index 6eeffc5eef..5c7193f1bb 100644 --- a/packages/examples/acp-demo/tsconfig.json +++ b/packages/examples/acp-demo/tsconfig.json @@ -18,7 +18,7 @@ "path": "../../../vendor/loader" }, { - "path": "../../ui/app-boot" + "path": "../../boot/app-boot" }, { "path": "../../acp/acp" @@ -39,10 +39,10 @@ "path": "../../context/workspace-context" }, { - "path": "../../session-persistence/session-checkpoint-policy" + "path": "../../session/session-checkpoint-policy" }, { - "path": "../../session-persistence/session-persistence-jsonl" + "path": "../../session/session-persistence-jsonl" }, { "path": "../../support/invariants" diff --git a/packages/examples/agent-spine-demo/tsconfig.json b/packages/examples/agent-spine-demo/tsconfig.json index f245e9f4d0..e31863d3b1 100644 --- a/packages/examples/agent-spine-demo/tsconfig.json +++ b/packages/examples/agent-spine-demo/tsconfig.json @@ -27,7 +27,7 @@ "path": "../../core/session" }, { - "path": "../../session-title/session-title" + "path": "../../session/session-title" }, { "path": "../../core/system-prompt" diff --git a/packages/examples/jsonrpc-demo/README.i18n.yaml b/packages/examples/jsonrpc-demo/README.i18n.yaml index 4e7935dd3a..c47938ce3b 100644 --- a/packages/examples/jsonrpc-demo/README.i18n.yaml +++ b/packages/examples/jsonrpc-demo/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/examples/jsonrpc-demo/README.md -README.md: 2cc496da3e22baf9e87afe4c3ca183ae3f16a5ae -README.zh.md: 98d397b3c346ee47e9a667b2ccec0179f009a05b +README.md: fff8e78698cd3d6320606084ef5c533be7c52633 +README.zh.md: 75382b97ea1837cf1415e8a7f5004206596e168c diff --git a/packages/examples/jsonrpc-demo/README.md b/packages/examples/jsonrpc-demo/README.md index 2cc496da3e..fff8e78698 100644 --- a/packages/examples/jsonrpc-demo/README.md +++ b/packages/examples/jsonrpc-demo/README.md @@ -2,11 +2,11 @@ English | [中文](README.zh.md) -Bin-only app that boots an external `cordis.yml`; its [`jsonrpc`](../../ui/jsonrpc/README.md) entry serves SDK clients over newline-delimited stdio. The config composes the spine, backends, and serving plugin. The published bin is `dsh-jsonrpc-agent`, and `lib/bin.js` also ships as the `dsh-jsonrpc-agent-pkg` [single-executable runtime](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) used by the Python SDK. +Bin-only app that boots an external `cordis.yml`; its [`jsonrpc`](../../scaffold/server/README.md) entry serves SDK clients over newline-delimited stdio. The config composes the spine, backends, and serving plugin. The published bin is `dsh-jsonrpc-agent`, and `lib/bin.js` also ships as the `dsh-jsonrpc-agent-pkg` [single-executable runtime](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) used by the Python SDK. ## Config discovery -The first non-empty channel wins: `$DSH_CORDIS_CONFIG`, then positional `argv[2]`. If neither names an existing file, the bin prints one-line usage to stderr and exits 1; there is no working-directory or built-in fallback. [`dsh-app-boot`](../../ui/app-boot/README.md) makes plugin load failures fatal. This protocol does not use `DSH_SNAPSHOT`. +The first non-empty channel wins: `$DSH_CORDIS_CONFIG`, then positional `argv[2]`. If neither names an existing file, the bin prints one-line usage to stderr and exits 1; there is no working-directory or built-in fallback. [`dsh-app-boot`](../../boot/app-boot/README.md) makes plugin load failures fatal. This protocol does not use `DSH_SNAPSHOT`. A config without `dsh-jsonrpc` is valid and serves nothing; the bin does not designate a server plugin. diff --git a/packages/examples/jsonrpc-demo/README.zh.md b/packages/examples/jsonrpc-demo/README.zh.md index 98d397b3c3..75382b97ea 100644 --- a/packages/examples/jsonrpc-demo/README.zh.md +++ b/packages/examples/jsonrpc-demo/README.zh.md @@ -2,11 +2,11 @@ [English](README.md) | 中文 -只包含 bin 的应用,启动外部 `cordis.yml`;其 [`jsonrpc`](../../ui/jsonrpc/README.md) 入口通过按换行分隔的 stdio 为 SDK 客户端提供服务。配置负责组合主干、后端和服务插件。发布的 bin 名为 `dsh-jsonrpc-agent`,`lib/bin.js` 还会作为 Python SDK 使用的 `dsh-jsonrpc-agent-pkg` [单文件可执行运行时](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)交付。 +只包含 bin 的应用,启动外部 `cordis.yml`;其 [`jsonrpc`](../../scaffold/server/README.md) 入口通过按换行分隔的 stdio 为 SDK 客户端提供服务。配置负责组合主干、后端和服务插件。发布的 bin 名为 `dsh-jsonrpc-agent`,`lib/bin.js` 还会作为 Python SDK 使用的 `dsh-jsonrpc-agent-pkg` [单文件可执行运行时](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)交付。 ## 配置发现 -第一个非空通道生效:先 `$DSH_CORDIS_CONFIG`,再位置参数 `argv[2]`。如果二者都没有指向现有文件,bin 会向 stderr 打印单行用法并以 1 退出;没有工作目录回退或内置回退。[`dsh-app-boot`](../../ui/app-boot/README.md) 会使插件加载失败成为致命错误。此协议不使用 `DSH_SNAPSHOT`。 +第一个非空通道生效:先 `$DSH_CORDIS_CONFIG`,再位置参数 `argv[2]`。如果二者都没有指向现有文件,bin 会向 stderr 打印单行用法并以 1 退出;没有工作目录回退或内置回退。[`dsh-app-boot`](../../boot/app-boot/README.md) 会使插件加载失败成为致命错误。此协议不使用 `DSH_SNAPSHOT`。 不含 `dsh-jsonrpc` 的配置仍然有效,只是不提供任何服务;bin 不会指定服务器插件。 diff --git a/packages/examples/jsonrpc-demo/tsconfig.json b/packages/examples/jsonrpc-demo/tsconfig.json index aba279d405..061bbf68c3 100644 --- a/packages/examples/jsonrpc-demo/tsconfig.json +++ b/packages/examples/jsonrpc-demo/tsconfig.json @@ -15,7 +15,7 @@ "path": "../../../vendor/loader" }, { - "path": "../../ui/app-boot" + "path": "../../boot/app-boot" }, { "path": "../../support/invariants" diff --git a/packages/feedback/README.i18n.yaml b/packages/feedback/README.i18n.yaml index 4ad5a93fb5..d0fe492135 100644 --- a/packages/feedback/README.i18n.yaml +++ b/packages/feedback/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/feedback/README.md -README.md: d2a4a5a27e1c661d2f62b328578fd890a0c622ee -README.zh.md: 2fa42e3bb5f05dfc425356f302f44e497b100f24 +README.md: af8e9d5c4903594299284d09f880aa8929f5e051 +README.zh.md: 01a26798d0b7dc8b376929252a60a99fff11b640 diff --git a/packages/feedback/README.md b/packages/feedback/README.md index d2a4a5a27e..af8e9d5c49 100644 --- a/packages/feedback/README.md +++ b/packages/feedback/README.md @@ -8,4 +8,4 @@ The feedback family lets a human record a remark about the session without actin |---|---|---| | `command-feedback/` | Trigger-independent `feedback/record` event plus the human-facing `/feedback` producer | — | -A recorded remark is log-only: it never enters the model surface or derived history. When mounted, [`dsh-session-telemetry-otel`](../telemetry/session-telemetry-otel/) observes `feedback/record` to release a pending telemetry prefix or warn that disabled telemetry leaves the feedback local; capture itself remains independent of that policy. +A recorded remark is log-only: it never enters the model surface or derived history. When mounted, [`dsh-session-telemetry-otel`](../session/session-telemetry-otel) observes `feedback/record` to release a pending telemetry prefix or warn that disabled telemetry leaves the feedback local; capture itself remains independent of that policy. diff --git a/packages/feedback/README.zh.md b/packages/feedback/README.zh.md index 2fa42e3bb5..01a26798d0 100644 --- a/packages/feedback/README.zh.md +++ b/packages/feedback/README.zh.md @@ -8,4 +8,4 @@ feedback 家族让人类记录对会话的评价,但不据此采取任何动 |---|---|---| | `command-feedback/` | 与触发方式无关的 `feedback/record` 事件,以及面向用户的 `/feedback` 生产方 | 无 | -被记录的评价仅写入日志:它绝不会进入模型 surface 或派生历史。挂载后,[`dsh-session-telemetry-otel`](../telemetry/session-telemetry-otel/) 会观察 `feedback/record`,以释放待处理的遥测前缀,或在遥测已禁用时警告反馈将留在本地;采集本身与该策略相互独立。 +被记录的评价仅写入日志:它绝不会进入模型 surface 或派生历史。挂载后,[`dsh-session-telemetry-otel`](../session/session-telemetry-otel) 会观察 `feedback/record`,以释放待处理的遥测前缀,或在遥测已禁用时警告反馈将留在本地;采集本身与该策略相互独立。 diff --git a/packages/feedback/command-feedback/README.i18n.yaml b/packages/feedback/command-feedback/README.i18n.yaml index ad645a61d3..20eb2741d6 100644 --- a/packages/feedback/command-feedback/README.i18n.yaml +++ b/packages/feedback/command-feedback/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/feedback/command-feedback/README.md -README.md: 1923267eb3a25a4be564fa4f4535f7a3459ca481 -README.zh.md: 674665167030aa6214e9acaa7e6f5314a78c5b14 +README.md: e2eb6d4cf2b40e83efad1fa158edd72578658f56 +README.zh.md: f87067d67cf9dcb8f4da97afe3a9d59eb828faf7 diff --git a/packages/feedback/command-feedback/README.md b/packages/feedback/command-feedback/README.md index 1923267eb3..e2eb6d4cf2 100644 --- a/packages/feedback/command-feedback/README.md +++ b/packages/feedback/command-feedback/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Trigger-independent session feedback plus human-facing `/feedback` capture. The package exports `recordFeedback(session, text)`, which appends one log-only `feedback/record` event. Its plugin registers one global command through [`ctx.commands`](../../ui/commands/README.md), so every composed command adapter discovers it; the shipped Web client executes it without a model turn. +Trigger-independent session feedback plus human-facing `/feedback` capture. The package exports `recordFeedback(session, text)`, which appends one log-only `feedback/record` event. Its plugin registers one global command through [`ctx.commands`](../../interaction/commands/README.md), so every composed command adapter discovers it; the shipped Web client executes it without a model turn. ## Command contract @@ -15,9 +15,9 @@ Surrounding whitespace is discarded, but feedback is otherwise unparsed: no trun ## What this plugin does and does not do -`recordFeedback(session, text)` is the command-independent write path. It rejects empty normalized text and appends `feedback/record { text }`; a different UI, hook, or host integration can call it without constructing a slash command. The `/feedback` handler uses that producer and starts no model work. The optional [`dsh-session-telemetry-otel`](../../telemetry/session-telemetry-otel/) consumer observes the event without changing its capture contract. +`recordFeedback(session, text)` is the command-independent write path. It rejects empty normalized text and appends `feedback/record { text }`; a different UI, hook, or host integration can call it without constructing a slash command. The `/feedback` handler uses that producer and starts no model work. The optional [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) consumer observes the event without changing its capture contract. -The feedback text appears in exactly one durable payload: `feedback/record`. [`dsh-commands`](../../ui/commands/README.md) still appends its generic `command/run` / `command/done` pairing, but this definition sets `recordInput: false`, so `command/run` omits `args`; the paired `command/done` carries only the outcome. All three events are log-only and absent from the ordered surface, `deriveMessages()`, and model requests. These appends start persistence's ordinary eager drain, but neither producer forces `session/flush`, so acknowledgement means the feedback is in the log, not that it has reached disk. Rejected empty input leaves only the command pairing settled as `kind: 'error'`, with no `feedback/record`. +The feedback text appears in exactly one durable payload: `feedback/record`. [`dsh-commands`](../../interaction/commands/README.md) still appends its generic `command/run` / `command/done` pairing, but this definition sets `recordInput: false`, so `command/run` omits `args`; the paired `command/done` carries only the outcome. All three events are log-only and absent from the ordered surface, `deriveMessages()`, and model requests. These appends start persistence's ordinary eager drain, but neither producer forces `session/flush`, so acknowledgement means the feedback is in the log, not that it has reached disk. Rejected empty input leaves only the command pairing settled as `kind: 'error'`, with no `feedback/record`. The event is authoritative rather than the command record because feedback may arrive through a trigger other than `/feedback`. Keeping the payload out of `command/run` avoids two records carrying the same text. diff --git a/packages/feedback/command-feedback/README.zh.md b/packages/feedback/command-feedback/README.zh.md index 6746651670..f87067d67c 100644 --- a/packages/feedback/command-feedback/README.zh.md +++ b/packages/feedback/command-feedback/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -与触发方式无关的会话反馈,以及面向用户的 `/feedback` 采集。本包导出 `recordFeedback(session, text)`,后者追加一个仅写入日志的 `feedback/record` 事件。该插件通过 [`ctx.commands`](../../ui/commands/README.md) 注册一个全局命令,因此每个已组合的命令适配器都能发现它;随附的 Web 客户端无需模型轮次即可执行。 +与触发方式无关的会话反馈,以及面向用户的 `/feedback` 采集。本包导出 `recordFeedback(session, text)`,后者追加一个仅写入日志的 `feedback/record` 事件。该插件通过 [`ctx.commands`](../../interaction/commands/README.md) 注册一个全局命令,因此每个已组合的命令适配器都能发现它;随附的 Web 客户端无需模型轮次即可执行。 ## 命令契约 @@ -15,9 +15,9 @@ ## 本插件做什么、不做什么 -`recordFeedback(session, text)` 是不依赖命令的写入路径。它拒绝规范化后为空的文本,并追加 `feedback/record { text }`;其他 UI、钩子或 host 集成无需构造斜杠命令即可调用它。`/feedback` 处理器通过该生产方写入,且不启动任何模型工作。可选的 [`dsh-session-telemetry-otel`](../../telemetry/session-telemetry-otel/) 消费方会观察该事件,但不改变它的采集契约。 +`recordFeedback(session, text)` 是不依赖命令的写入路径。它拒绝规范化后为空的文本,并追加 `feedback/record { text }`;其他 UI、钩子或 host 集成无需构造斜杠命令即可调用它。`/feedback` 处理器通过该生产方写入,且不启动任何模型工作。可选的 [`dsh-session-telemetry-otel`](../../session/session-telemetry-otel) 消费方会观察该事件,但不改变它的采集契约。 -反馈文本只出现在一个持久载荷中:`feedback/record`。[`dsh-commands`](../../ui/commands/README.md) 仍会追加通用的 `command/run` / `command/done` 配对,但此定义设置了 `recordInput: false`,因此 `command/run` 会省略 `args`;配对的 `command/done` 只携带结果。三个事件都仅写入日志,不出现在有序 surface、`deriveMessages()` 以及模型请求中。这些追加会启动持久化的常规即时排空,但两个生产方都不会强制 `session/flush`,因此确认文本表示反馈已进入日志,而不表示它已经落盘。被拒绝的空输入只会留下以 `kind: 'error'` 结算的命令配对,不会产生 `feedback/record`。 +反馈文本只出现在一个持久载荷中:`feedback/record`。[`dsh-commands`](../../interaction/commands/README.md) 仍会追加通用的 `command/run` / `command/done` 配对,但此定义设置了 `recordInput: false`,因此 `command/run` 会省略 `args`;配对的 `command/done` 只携带结果。三个事件都仅写入日志,不出现在有序 surface、`deriveMessages()` 以及模型请求中。这些追加会启动持久化的常规即时排空,但两个生产方都不会强制 `session/flush`,因此确认文本表示反馈已进入日志,而不表示它已经落盘。被拒绝的空输入只会留下以 `kind: 'error'` 结算的命令配对,不会产生 `feedback/record`。 权威记录是该事件,而不是命令记录,因为反馈可能来自 `/feedback` 之外的触发方式。让载荷不进入 `command/run`,可避免两条记录携带相同文本。 diff --git a/packages/feedback/command-feedback/tsconfig.json b/packages/feedback/command-feedback/tsconfig.json index 0a99f13f01..f59431af9a 100644 --- a/packages/feedback/command-feedback/tsconfig.json +++ b/packages/feedback/command-feedback/tsconfig.json @@ -15,7 +15,7 @@ "path": "../../../vendor/cordis" }, { - "path": "../../ui/commands" + "path": "../../interaction/commands" }, { "path": "../../core/session" diff --git a/packages/fs/README.i18n.yaml b/packages/fs/README.i18n.yaml index f4719447aa..b1e493f3c8 100644 --- a/packages/fs/README.i18n.yaml +++ b/packages/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/README.md -README.md: b15012e882b60847e1ad22edf08d1202ba64fe5b -README.zh.md: 628f6c74894bc67559d49f7cf5d1378d0ece2382 +README.md: 162e2c75e92bc4ea6aba377c14db62e8ce83fbb9 +README.zh.md: b2929b17be6ce90051f0808c2cdf4bbf643f8f13 diff --git a/packages/fs/README.md b/packages/fs/README.md index b15012e882..162e2c75e9 100644 --- a/packages/fs/README.md +++ b/packages/fs/README.md @@ -19,3 +19,5 @@ The interface lives at `fs/fs/`. A sandboxed, remote, or project-scoped filesyst ## No timeouts on file IO `read`/`write`/`edit` take **no** `timeoutMs`, and the provider seam arms no deadline — unlike bash and web (which consume [`@deepseek-ai/dsh-timeout`](../util/timeout/README.md)) and the bash-backed `glob`/`grep` (whose declared `timeoutMs` is enforced by `@deepseek-ai/dsh-timeout-policy`): those are process-backed, where a deadline can really kill the work. A local syscall is best-effort-abortable at most: a timeout could not force an in-progress `fsync`/`rename` to stop, so a deadline here would be a knob that cannot deliver on its promise. Adding one would also be an implicit default in the exact place explicit-over-implicit forbids. Both reference agents (Claude Code, Codex) leave file IO untimed for the same reason; cancellation still propagates through the tool-execution signal for best-effort abort at syscall boundaries. + +The subsystem reference — targets, outcomes, guards, policy events, the error taxonomy, and why file IO takes no timeout — is [docs/subsystems/filesystem.md](../../docs/subsystems/filesystem.md); the sandbox fence in the [cross-family fs sandbox Agent Note](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md). diff --git a/packages/fs/README.zh.md b/packages/fs/README.zh.md index 628f6c7489..b2929b17be 100644 --- a/packages/fs/README.zh.md +++ b/packages/fs/README.zh.md @@ -19,3 +19,5 @@ ## 文件 I/O 不设超时 `read`/`write`/`edit` **不** 接受 `timeoutMs`,提供方 seam 也不启动 deadline。这与 bash 和 web(两者使用 [`@deepseek-ai/dsh-timeout`](../util/timeout/README.md))及基于 bash 的 `glob`/`grep` 不同(它们声明的 `timeoutMs` 由 `@deepseek-ai/dsh-timeout-policy` 强制执行):这些工作由进程支持,deadline 可以实际终止工作。本地系统调用至多只能尽力中止:超时无法强制正在进行的 `fsync`/`rename` 停止,因此这里的 deadline 会成为无法兑现承诺的配置项。在此添加 deadline 还会在「显式优于隐式」明确禁止的地方引入隐式默认值。两个参考 agent(Claude Code、Codex)出于同一原因都不为文件 I/O 计时;取消仍通过工具执行信号传播,在系统调用边界尽力中止。 + +子系统参考——目标、结果、防护、策略事件、错误分类体系,以及文件 IO 为何不设超时——见 [docs/subsystems/filesystem.md](../../docs/subsystems/filesystem.md);沙箱围栏见[跨家族 fs 沙箱 Agent Note](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)。 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/fs/tool-fs/tsconfig.json b/packages/fs/tool-fs/tsconfig.json index fb420b553c..a3f128143c 100644 --- a/packages/fs/tool-fs/tsconfig.json +++ b/packages/fs/tool-fs/tsconfig.json @@ -40,7 +40,7 @@ "path": "../../sandbox/sandbox-policy" }, { - "path": "../../ui/user-approval" + "path": "../../interaction/user-approval" } ] } diff --git a/packages/goal/README.i18n.yaml b/packages/goal/README.i18n.yaml index 4238bac3c4..d2a6bf0248 100644 --- a/packages/goal/README.i18n.yaml +++ b/packages/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/README.md -README.md: 9fc6b0c18b1862a8be08275785ea3b6185e9bdbc -README.zh.md: 08f15bcc4e405e25d4dfd5981bbc99408833403c +README.md: 9d307278b39ecee61df9feace9fec4235b45d794 +README.zh.md: 7217c174ba5382c53204ae3678be42375fc63db5 diff --git a/packages/goal/README.md b/packages/goal/README.md index 9fc6b0c18b..9d307278b3 100644 --- a/packages/goal/README.md +++ b/packages/goal/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The goal family owns durable objective state independently of the model-facing tools and continuation policy that consume it. +Durable objective state for an agent session, owned independently of the model-facing tools and continuation policy that consume it. Goal state is part of the owning session log; consumers depend on `dsh-goal`, never on the concrete agent loop. | Package | Role | ctx key | |---|---|---| @@ -11,4 +11,4 @@ The goal family owns durable objective state independently of the model-facing t | [`tool-goal/`](tool-goal/README.md) | Model-facing goal tools | — | | [`command-goal/`](command-goal/README.md) | Human-facing goal command | — | -Goal state is part of the owning session log. Consumers depend on `dsh-goal`, not on the concrete agent loop; continuation behavior belongs in a separate plugin on the public agent seams. +The subsystem reference — goal identity, lifecycle snapshots, activation, change records — is [docs/subsystems/goal.md](../../docs/subsystems/goal.md). diff --git a/packages/goal/README.zh.md b/packages/goal/README.zh.md index 08f15bcc4e..7217c174ba 100644 --- a/packages/goal/README.zh.md +++ b/packages/goal/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -goal 家族负责持久目标状态,与消费该状态的面向模型工具和续行策略相互独立。 +agent 会话的持久目标状态,独立于消费它的模型工具与续跑策略。goal 状态是所属会话日志的一部分;消费方依赖 `dsh-goal`,绝不依赖具体 agent loop。 | 包 | 职责 | ctx 键 | |---|---|---| @@ -11,4 +11,4 @@ goal 家族负责持久目标状态,与消费该状态的面向模型工具和 | [`tool-goal/`](tool-goal/README.md) | 面向模型的目标工具 | 无 | | [`command-goal/`](command-goal/README.md) | 面向用户的目标命令 | 无 | -目标状态是其所属会话日志的一部分。消费方依赖 `dsh-goal`,而不是具体的 agent loop(智能体循环);续行行为由基于公开 agent seam 的独立插件负责。 +子系统参考——goal 标识、生命周期快照、激活、变更记录——见 [docs/subsystems/goal.md](../../docs/subsystems/goal.md)。 diff --git a/packages/goal/command-goal/README.i18n.yaml b/packages/goal/command-goal/README.i18n.yaml index 6586a5eda8..8f238d7429 100644 --- a/packages/goal/command-goal/README.i18n.yaml +++ b/packages/goal/command-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/command-goal/README.md -README.md: a02803b3ef7f93f0c4910ec2be662cc7836d9048 -README.zh.md: 9c6d0cc6e7309a138e17f1a2d5ad6c5285913394 +README.md: 1ac1b8a0e2cb360708b1759ab5eb9ec3de597821 +README.zh.md: cc5f7ba9bd3c5e50d3b7ce204d8d8c400e4f0955 diff --git a/packages/goal/command-goal/README.md b/packages/goal/command-goal/README.md index a02803b3ef..1ac1b8a0e2 100644 --- a/packages/goal/command-goal/README.md +++ b/packages/goal/command-goal/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Human-facing `/goal` control over [`ctx.goals`](../goal/README.md). The plugin registers one global command through [`ctx.commands`](../../ui/commands/README.md), so every composed command adapter discovers and executes it without a model turn. The [human goal-command Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-human-goal-command.md) owns the UX and composition decisions. +Human-facing `/goal` control over [`ctx.goals`](../goal/README.md). The plugin registers one global command through [`ctx.commands`](../../interaction/commands/README.md), so every composed command adapter discovers and executes it without a model turn. The [human goal-command Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-human-goal-command.md) owns the UX and composition decisions. ## Command contract diff --git a/packages/goal/command-goal/README.zh.md b/packages/goal/command-goal/README.zh.md index 9c6d0cc6e7..cc5f7ba9bd 100644 --- a/packages/goal/command-goal/README.zh.md +++ b/packages/goal/command-goal/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -面向用户的 `/goal` 控制,基于 [`ctx.goals`](../goal/README.md) 实现。该插件通过 [`ctx.commands`](../../ui/commands/README.md) 注册一个全局命令,因此每个已组合的命令适配器都能发现并执行它,无需模型轮次。[用户 goal 命令 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-human-goal-command.md)负责用户体验与组合决策。 +面向用户的 `/goal` 控制,基于 [`ctx.goals`](../goal/README.md) 实现。该插件通过 [`ctx.commands`](../../interaction/commands/README.md) 注册一个全局命令,因此每个已组合的命令适配器都能发现并执行它,无需模型轮次。[用户 goal 命令 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-human-goal-command.md)负责用户体验与组合决策。 ## 命令契约 diff --git a/packages/goal/command-goal/tsconfig.json b/packages/goal/command-goal/tsconfig.json index 8b03235814..488c576922 100644 --- a/packages/goal/command-goal/tsconfig.json +++ b/packages/goal/command-goal/tsconfig.json @@ -15,7 +15,7 @@ "path": "../../../vendor/cordis" }, { - "path": "../../ui/commands" + "path": "../../interaction/commands" }, { "path": "../goal" diff --git a/packages/goal/goal/README.i18n.yaml b/packages/goal/goal/README.i18n.yaml index d918caf377..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: fc2a672c11c68ad72437251b087a274e1c4388d3 -README.zh.md: eaaae5b333151b1d936effc593b21cac515471e2 +README.md: 98765a809820bcd4aeba31e1dd4ef6e92eb30760 +README.zh.md: dbc499104bac6cf09f9b1c871143da8781a8f082 diff --git a/packages/goal/goal/README.md b/packages/goal/goal/README.md index fc2a672c11..98765a8098 100644 --- a/packages/goal/goal/README.md +++ b/packages/goal/goal/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Event-sourced same-session goal state. The service retains one current completion objective in an agent's existing session while keeping permission to continue as process-local activation. The [goal-domain Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) owns the design rationale; the [goal type catalog](../../../docs/core-data-structures/goal.md) records the literal data shapes. +Event-sourced same-session goal state. The service retains one current completion objective in an agent's existing session while keeping permission to continue as process-local activation. The [goal-domain Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) owns the design rationale; the [goal type catalog](../../../docs/subsystems/goal.md) records the literal data shapes. ## Config @@ -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 eaaae5b333..dbc499104b 100644 --- a/packages/goal/goal/README.zh.md +++ b/packages/goal/goal/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -事件溯源的同会话目标状态。该服务在 agent(智能体)的现有会话中保留一个当前完成目标,同时将继续执行的权限作为进程本地续行启用状态。[goal 领域 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) 负责设计理由;[goal 类型目录](../../../docs/core-data-structures/goal.md)记录具体的数据形状。 +事件溯源的同会话目标状态。该服务在 agent(智能体)的现有会话中保留一个当前完成目标,同时将继续执行的权限作为进程本地续行启用状态。[goal 领域 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) 负责设计理由;[goal 类型目录](../../../docs/subsystems/goal.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/goal/goal/tsconfig.json b/packages/goal/goal/tsconfig.json index f106707bd3..1484f5a4b9 100644 --- a/packages/goal/goal/tsconfig.json +++ b/packages/goal/goal/tsconfig.json @@ -33,7 +33,7 @@ "path": "../../core/agent" }, { - "path": "../../session-projection/session-projection" + "path": "../../session/session-projection" }, { "path": "../../typert/type-meta" diff --git a/packages/guard/README.i18n.yaml b/packages/guard/README.i18n.yaml index 637a36063e..b99cec734b 100644 --- a/packages/guard/README.i18n.yaml +++ b/packages/guard/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/guard/README.md -README.md: 8da2f2ddcc1432e7963e600b35549f5e26121af2 -README.zh.md: aa8b5bd1f7d10d90d1dc84d07a7aad81bfb4259c +README.md: f0a80b1f497ee5b2459bebb55f886b42d74e4323 +README.zh.md: c7747e2febcb1656925fd556743eb2dd28090e37 diff --git a/packages/guard/README.md b/packages/guard/README.md index 8da2f2ddcc..f0a80b1f49 100644 --- a/packages/guard/README.md +++ b/packages/guard/README.md @@ -2,8 +2,11 @@ English | [中文](README.zh.md) -Behavioral guard plugins watch the agent loop for unproductive patterns and nudge the model back on course. A guard is a self-contained consumer of core seams, not a swappable capability. +Behavioral guard plugins watch the agent loop for unproductive patterns and enforce per-call budgets. A guard is a self-contained consumer of core seams, not a swappable capability. | Package | Role | ctx key | |---|---|---| | [`repeat-tool-guard/`](repeat-tool-guard/README.md) | Advisory reminders for repeated tool calls | listens on tool and agent events | +| [`timeout-policy/`](timeout-policy/README.md) | Arms per-call tool deadlines as deployment policy | registers a `tools/execute` listener | + +Reminders travel as `additionalContexts` on the `tools/post-execute` decision and are appended as logged plugin-sourced `user/message` events ([tools](../../docs/subsystems/tools.md)); the timeout split across `dsh-timeout`, capability termination, and this policy layer is recorded in the [timeout-library Agent Note](../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md). diff --git a/packages/guard/README.zh.md b/packages/guard/README.zh.md index aa8b5bd1f7..c7747e2feb 100644 --- a/packages/guard/README.zh.md +++ b/packages/guard/README.zh.md @@ -2,8 +2,11 @@ [English](README.md) | 中文 -行为 guard 插件监视 agent loop(智能体循环)中的无效模式,并推动模型回到正轨。guard 是 core seam 的自包含消费方,而非可替换能力。 +行为 guard 插件监视 agent loop(智能体循环)中的无效模式,并强制执行单次调用预算。guard 是 core seam 的自包含消费方,而非可替换能力。 | 包 | 职责 | ctx key | |---|---|---| | [`repeat-tool-guard/`](repeat-tool-guard/README.md) | 针对重复工具调用的建议性提醒 | 监听工具和 agent 事件 | +| [`timeout-policy/`](timeout-policy/README.md) | 以部署策略形式设置单次工具调用截止时间 | 注册 `tools/execute` 监听器 | + +提醒作为 `additionalContexts` 随 `tools/post-execute` 决策传递,并以插件来源的 `user/message` 事件记录([工具](../../docs/subsystems/tools.md));跨 `dsh-timeout`、能力方终止与本策略层的超时拆分记录在[超时库 Agent Note](../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)。 diff --git a/packages/timeout/timeout-policy/README.i18n.yaml b/packages/guard/timeout-policy/README.i18n.yaml similarity index 79% rename from packages/timeout/timeout-policy/README.i18n.yaml rename to packages/guard/timeout-policy/README.i18n.yaml index 377d2c528d..3c41810118 100644 --- a/packages/timeout/timeout-policy/README.i18n.yaml +++ b/packages/guard/timeout-policy/README.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 packages/timeout/timeout-policy/README.md +# pnpm run verify-translation-pairing --write packages/guard/timeout-policy/README.md README.md: 3e5769e2f8b95392e9d489659c6bae634ab3ff18 README.zh.md: bfba62cdfae8bb956aec43bcf6e8df12407d8ba2 diff --git a/packages/timeout/timeout-policy/README.md b/packages/guard/timeout-policy/README.md similarity index 100% rename from packages/timeout/timeout-policy/README.md rename to packages/guard/timeout-policy/README.md diff --git a/packages/timeout/timeout-policy/README.zh.md b/packages/guard/timeout-policy/README.zh.md similarity index 100% rename from packages/timeout/timeout-policy/README.zh.md rename to packages/guard/timeout-policy/README.zh.md diff --git a/packages/timeout/timeout-policy/package.json b/packages/guard/timeout-policy/package.json similarity index 100% rename from packages/timeout/timeout-policy/package.json rename to packages/guard/timeout-policy/package.json diff --git a/packages/timeout/timeout-policy/src/index.ts b/packages/guard/timeout-policy/src/index.ts similarity index 91% rename from packages/timeout/timeout-policy/src/index.ts rename to packages/guard/timeout-policy/src/index.ts index 61470ea6af..f554b1a93e 100644 --- a/packages/timeout/timeout-policy/src/index.ts +++ b/packages/guard/timeout-policy/src/index.ts @@ -2,6 +2,12 @@ * Cooperative tool-call timeout enforcer. A tool declares `timeoutMs` and * promises to honor `exec.signal`; this wrapper arms that deadline and maps its * own expiry to `TOOL_TIMEOUT` without racing or abandoning the tool promise. + * + * FIXME: settle the intended `@deepseek-ai/dsh-timeout-guard` rename before the + * first tagged release — suggestion only, aligning the name with its `guard/` + * home; decide at resolution time + * ([regrouping Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md)). + * * @module @deepseek-ai/dsh-timeout-policy */ diff --git a/packages/timeout/timeout-policy/src/invariant.ts b/packages/guard/timeout-policy/src/invariant.ts similarity index 100% rename from packages/timeout/timeout-policy/src/invariant.ts rename to packages/guard/timeout-policy/src/invariant.ts diff --git a/packages/timeout/timeout-policy/tests/timeout-policy.spec.ts b/packages/guard/timeout-policy/tests/timeout-policy.spec.ts similarity index 100% rename from packages/timeout/timeout-policy/tests/timeout-policy.spec.ts rename to packages/guard/timeout-policy/tests/timeout-policy.spec.ts diff --git a/packages/timeout/timeout-policy/tsconfig.json b/packages/guard/timeout-policy/tsconfig.json similarity index 100% rename from packages/timeout/timeout-policy/tsconfig.json rename to packages/guard/timeout-policy/tsconfig.json diff --git a/packages/hooks/hooks-claude/tsconfig.json b/packages/hooks/hooks-claude/tsconfig.json index 445d0f68b1..e254c7146f 100644 --- a/packages/hooks/hooks-claude/tsconfig.json +++ b/packages/hooks/hooks-claude/tsconfig.json @@ -30,7 +30,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { "path": "../../subagent/subagent" diff --git a/packages/hooks/hooks-codex/tsconfig.json b/packages/hooks/hooks-codex/tsconfig.json index 3bd9bd91e5..46e8216404 100644 --- a/packages/hooks/hooks-codex/tsconfig.json +++ b/packages/hooks/hooks-codex/tsconfig.json @@ -30,7 +30,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { "path": "../../llm/llm" diff --git a/packages/host/README.i18n.yaml b/packages/host/README.i18n.yaml index 1aaacd7ecb..0859a2571d 100644 --- a/packages/host/README.i18n.yaml +++ b/packages/host/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/host/README.md -README.md: 269a27f51c842f13bc11c175916b7be22db72bd2 -README.zh.md: 559bf785eb45d59a30f676b98c14143c69d57edd +README.md: 926cb0b6b87a8ee76cb2dab745a31f620f4e7f5c +README.zh.md: dc617084c756b80716f9a02bd2d22ba6954030f6 diff --git a/packages/host/README.md b/packages/host/README.md index 269a27f51c..926cb0b6b8 100644 --- a/packages/host/README.md +++ b/packages/host/README.md @@ -15,3 +15,5 @@ The host side of the dsh web GUI: the API gateway every client shape shares, and | [`directory-picker-auto/`](directory-picker-auto/README.md) | Host-adaptive picker composition | mounts a backend | `apiproxy` remains transport-independent; [`client/connection`](../client/connection/README.md) supplies the browser/HTTP carrier. Picker implementations replace one another behind the shared seam. + +The subsystem references: [http-server.md](../../docs/subsystems/http-server.md) and [workspace.md](../../docs/subsystems/workspace.md) (the picker seam). diff --git a/packages/host/README.zh.md b/packages/host/README.zh.md index 559bf785eb..dc617084c7 100644 --- a/packages/host/README.zh.md +++ b/packages/host/README.zh.md @@ -15,3 +15,5 @@ dsh Web GUI 的宿主侧:所有客户端形态共享的 API 网关,以及承 | [`directory-picker-auto/`](directory-picker-auto/README.md) | 宿主自适应选择器组合 | 挂载一个后端 | `apiproxy` 保持传输无关;[`client/connection`](../client/connection/README.md) 提供浏览器/HTTP 载体。选择器实现可在共享 seam 后互相替换。 + +子系统参考:[http-server.md](../../docs/subsystems/http-server.md) 与 [workspace.md](../../docs/subsystems/workspace.md)(挑选 seam)。 diff --git a/packages/host/apiproxy/tsconfig.json b/packages/host/apiproxy/tsconfig.json index 101e842bdd..cd4c9894a5 100644 --- a/packages/host/apiproxy/tsconfig.json +++ b/packages/host/apiproxy/tsconfig.json @@ -45,19 +45,19 @@ "path": "../../core/tools" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { - "path": "../../session-projection/session-projection" + "path": "../../session/session-projection" }, { - "path": "../../session-projection/session-projection-cache" + "path": "../../session/session-projection-cache" }, { "path": "../../session-query/session-query" }, { - "path": "../../session-title/session-title" + "path": "../../session/session-title" }, { "path": "../../session-query/session-query" @@ -69,13 +69,13 @@ "path": "../../skill/skill" }, { - "path": "../../ui/commands" + "path": "../../interaction/commands" }, { - "path": "../../ui/user-approval" + "path": "../../interaction/user-approval" }, { - "path": "../../ui/user-interaction" + "path": "../../interaction/user-interaction" }, { "path": "../../workspace/workspace" diff --git a/packages/host/webserver/README.i18n.yaml b/packages/host/webserver/README.i18n.yaml index 56fd0e7694..bd80f27f3e 100644 --- a/packages/host/webserver/README.i18n.yaml +++ b/packages/host/webserver/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/host/webserver/README.md -README.md: b6dccf2f81c9e2f0b9f53264eafe724edb560f07 -README.zh.md: dbfe420013ed67c48e47048341f020864aeef16a +README.md: 569c3f0c19db2c308beaef35baaf915fd39768cd +README.zh.md: b311ebffa3fb067e684e9e443c2ac02dab917d61 diff --git a/packages/host/webserver/README.md b/packages/host/webserver/README.md index b6dccf2f81..569c3f0c19 100644 --- a/packages/host/webserver/README.md +++ b/packages/host/webserver/README.md @@ -8,8 +8,6 @@ The package knows no harness concepts and serves no files: the `/api` HTTP bridg A listen failure (EADDRINUSE…) throws out of activation and rejects Loader composition with the bind diagnostic; the failed candidate fiber is disposed. An HTTP request whose handling throws (a fallback owner's `decodeURIComponent` on a malformed %-escape, a client dropping mid-body) is answered 400 — or the socket destroyed when headers are already out — and logged as a warning; it never exits the process. An upgrade-handler exception or upgraded-socket transport error is logged as a warning and destroys its socket. Disposal starts `close()` and `closeAllConnections()`, destroys every tracked upgraded socket, and returns only after the HTTP server and those sockets have closed. -In development, the client-plugin registry synchronously captures each built bundle's stat baseline before it returns, then polls those baselines and re-hashes changed content. Each rescan stages its candidate table, graph, and watch map before publishing them, so a baseline failure preserves the prior graph. An immediate rebuild therefore cannot disappear into an asynchronously established watch baseline; a rename window marks the path dirty, retains the last successful baseline, and forces a re-hash when the bundle reappears even with identical metadata. - ## Model Experience None, as the package is a Web carrier between the browser and the HTTP/upgrade routes other plugins register; nothing here reaches a model request. diff --git a/packages/host/webserver/README.zh.md b/packages/host/webserver/README.zh.md index dbfe420013..b311ebffa3 100644 --- a/packages/host/webserver/README.zh.md +++ b/packages/host/webserver/README.zh.md @@ -8,8 +8,6 @@ Web HTTP 与 upgrade route 注册插件(默认导出 `HttpServerService`,配 监听失败(EADDRINUSE……)会从激活过程抛出,以 bind 诊断使 Loader 组合 reject;失败的候选 fiber 会被 dispose(资源释放)。处理 HTTP 请求时抛错(例如 fallback 持有者的 `decodeURIComponent` 收到格式错误的百分号转义,或客户端在请求体传输中途断开)时,服务器会响应 400;若响应头已经发出,则销毁 socket,并记录 warning,但绝不会退出进程。upgrade handler 抛错或升级 socket 出现传输错误时,会记录 warning 并销毁对应 socket。资源释放会启动 `close()` 与 `closeAllConnections()`,销毁所有受跟踪的升级 socket,并仅在 HTTP server 与这些 socket 均已关闭后返回。 -在开发环境中,客户端插件注册表会在返回前同步捕获每个已构建 bundle 的 stat 基线,随后轮询这些基线,并在内容变化后重新计算哈希。每次重新扫描都会先暂存候选表、图和监听 map,再统一发布,因此基线失败会保留先前的图。这样,即时重建不会消失在异步建立的监听基线中;重命名窗口会把路径标记为脏,保留最近一次成功基线,并在 bundle 重新出现时强制重新计算哈希,即使其元数据完全相同也不例外。 - ## 模型体验 无。该包只是浏览器与其他插件所注册 HTTP/upgrade route 之间的 Web 载体,其中没有任何内容会进入模型请求。 diff --git a/packages/interaction/README.i18n.yaml b/packages/interaction/README.i18n.yaml new file mode 100644 index 0000000000..f2321e732a --- /dev/null +++ b/packages/interaction/README.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 packages/interaction/README.md +README.md: a72cd48bb1d3ec15b1116d576fac86619df59528 +README.zh.md: 170a557e40befaa0f7073f25e8a2b372958a6ec1 diff --git a/packages/ui/README.md b/packages/interaction/README.md similarity index 61% rename from packages/ui/README.md rename to packages/interaction/README.md index 15754410a4..a72cd48bb1 100644 --- a/packages/ui/README.md +++ b/packages/interaction/README.md @@ -1,8 +1,8 @@ -# ui/ — human and SDK-client integration surfaces +# interaction/ — the human-collaboration plane English | [中文](README.zh.md) -Human-facing channels and the out-of-process SDK server. These are **product** packages: real interfaces that a person or SDK client drives. +The seams through which a human collaborates with a running agent — questions, approvals, permission presets, commands. These are **product** packages: real interfaces a person drives. | Package | Role | ctx key | |---|---|---| @@ -11,7 +11,7 @@ Human-facing channels and the out-of-process SDK server. These are **product** p | [`permission/`](permission/README.md) | Presents and persists user-facing permission presets. | `ctx.permission` | | [`user-interaction/`](user-interaction/README.md) | Defines the provider-neutral human question/answer seam. | `ctx.userInteraction` | | [`tool-ask-user/`](tool-ask-user/README.md) | Exposes human questions to the model. | (registers on `ctx.tools`) | -| [`jsonrpc/`](jsonrpc/README.md) | Serves out-of-process SDK clients over stdio JSON-RPC. | (drives `ctx.agents`) | -| [`app-boot/`](app-boot/README.md) | Provides shared boot support for application launchers. | (library for the bins) | These packages integrate through existing agent and session contracts rather than changing the loop. Interactive applications provide the concrete command, approval, and question adapters; automation uses [`acp/`](../acp/README.md), and runnable demo bundles live under [`examples/`](../examples/README.md). The product [`dsh`](../../apps/cli/README.md) CLI composes these packages directly. + +The subsystem references: [approval.md](../../docs/subsystems/approval.md), [permission.md](../../docs/subsystems/permission.md), [user-interaction.md](../../docs/subsystems/user-interaction.md), and [commands.md](../../docs/subsystems/commands.md). The automation-only ACP transport is [`acp/`](../acp/README.md), the SDK's JSON-RPC server half [`scaffold/server`](../scaffold/README.md), and the shared bin boot glue [`boot/`](../boot/README.md). diff --git a/packages/ui/README.zh.md b/packages/interaction/README.zh.md similarity index 59% rename from packages/ui/README.zh.md rename to packages/interaction/README.zh.md index 4023b80085..170a557e40 100644 --- a/packages/ui/README.zh.md +++ b/packages/interaction/README.zh.md @@ -1,8 +1,8 @@ -# ui/:面向用户和 SDK 客户端的集成接口 +# interaction/:人机协作平面 [English](README.md) | 中文 -面向用户的通道和进程外 SDK 服务器。这些是**产品**包:由用户或 SDK 客户端直接操作的真实接口。 +人与运行中的 agent(智能体)协作所经由的各个 seam——提问、审批、权限预设、命令。这些是**产品**包(package):由用户直接操作的真实接口。 | 包 | 职责 | ctx 键 | |---|---|---| @@ -11,7 +11,7 @@ | [`permission/`](permission/README.md) | 呈现并持久化面向用户的权限预设。 | `ctx.permission` | | [`user-interaction/`](user-interaction/README.md) | 定义与提供方无关的用户问答 seam。 | `ctx.userInteraction` | | [`tool-ask-user/`](tool-ask-user/README.md) | 向模型公开用户问题。 | (注册到 `ctx.tools`) | -| [`jsonrpc/`](jsonrpc/README.md) | 通过 stdio JSON-RPC 为进程外 SDK 客户端提供服务。 | (驱动 `ctx.agents`) | -| [`app-boot/`](app-boot/README.md) | 为应用启动器提供共享启动支持。 | (供各 bin 使用的库) | 这些包通过现有的 agent(智能体)和会话契约集成,而不改变循环。交互式应用提供具体的命令、审批和提问适配器;自动化使用 [`acp/`](../acp/README.md),可运行的演示组合包位于 [`examples/`](../examples/README.md)。产品 [`dsh`](../../apps/cli/README.md) CLI(命令行界面)直接组合这些包。 + +子系统参考:[approval.md](../../docs/subsystems/approval.md)、[permission.md](../../docs/subsystems/permission.md)、[user-interaction.md](../../docs/subsystems/user-interaction.md)与 [commands.md](../../docs/subsystems/commands.md)。仅自动化的 ACP 传输在 [`acp/`](../acp/README.md),SDK 的 JSON-RPC 服务器一半在 [`scaffold/server`](../scaffold/README.md),共享 bin 启动胶水在 [`boot/`](../boot/README.md)。 diff --git a/packages/ui/commands/README.i18n.yaml b/packages/interaction/commands/README.i18n.yaml similarity index 79% rename from packages/ui/commands/README.i18n.yaml rename to packages/interaction/commands/README.i18n.yaml index be55a19ca3..967fa0d4ef 100644 --- a/packages/ui/commands/README.i18n.yaml +++ b/packages/interaction/commands/README.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 packages/ui/commands/README.md +# pnpm run verify-translation-pairing --write packages/interaction/commands/README.md README.md: 1709bdcdce4e43d98cfea5ff3972ab95bfd3c33b README.zh.md: 569f2aa8293793b26d63ee16e3ea7600e04a8397 diff --git a/packages/ui/commands/README.md b/packages/interaction/commands/README.md similarity index 100% rename from packages/ui/commands/README.md rename to packages/interaction/commands/README.md diff --git a/packages/ui/commands/README.zh.md b/packages/interaction/commands/README.zh.md similarity index 100% rename from packages/ui/commands/README.zh.md rename to packages/interaction/commands/README.zh.md diff --git a/packages/ui/commands/package.json b/packages/interaction/commands/package.json similarity index 100% rename from packages/ui/commands/package.json rename to packages/interaction/commands/package.json diff --git a/packages/ui/commands/src/brand.ts b/packages/interaction/commands/src/brand.ts similarity index 100% rename from packages/ui/commands/src/brand.ts rename to packages/interaction/commands/src/brand.ts diff --git a/packages/ui/commands/src/index.ts b/packages/interaction/commands/src/index.ts similarity index 100% rename from packages/ui/commands/src/index.ts rename to packages/interaction/commands/src/index.ts diff --git a/packages/ui/commands/src/invariant.ts b/packages/interaction/commands/src/invariant.ts similarity index 100% rename from packages/ui/commands/src/invariant.ts rename to packages/interaction/commands/src/invariant.ts diff --git a/packages/ui/commands/tests/commands.spec.ts b/packages/interaction/commands/tests/commands.spec.ts similarity index 100% rename from packages/ui/commands/tests/commands.spec.ts rename to packages/interaction/commands/tests/commands.spec.ts diff --git a/packages/ui/commands/tests/invariant.spec.ts b/packages/interaction/commands/tests/invariant.spec.ts similarity index 100% rename from packages/ui/commands/tests/invariant.spec.ts rename to packages/interaction/commands/tests/invariant.spec.ts diff --git a/packages/ui/commands/tsconfig.json b/packages/interaction/commands/tsconfig.json similarity index 100% rename from packages/ui/commands/tsconfig.json rename to packages/interaction/commands/tsconfig.json diff --git a/packages/ui/permission/README.i18n.yaml b/packages/interaction/permission/README.i18n.yaml similarity index 79% rename from packages/ui/permission/README.i18n.yaml rename to packages/interaction/permission/README.i18n.yaml index cb48934488..c8e4b28311 100644 --- a/packages/ui/permission/README.i18n.yaml +++ b/packages/interaction/permission/README.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 packages/ui/permission/README.md +# pnpm run verify-translation-pairing --write packages/interaction/permission/README.md README.md: 4f7f560bb81eaad3b6b95b2742432fa252682d5a README.zh.md: d45f89e243ce2d8f6bb08943fb7e776ced106b5a diff --git a/packages/ui/permission/README.md b/packages/interaction/permission/README.md similarity index 100% rename from packages/ui/permission/README.md rename to packages/interaction/permission/README.md diff --git a/packages/ui/permission/README.zh.md b/packages/interaction/permission/README.zh.md similarity index 100% rename from packages/ui/permission/README.zh.md rename to packages/interaction/permission/README.zh.md diff --git a/packages/ui/permission/package.json b/packages/interaction/permission/package.json similarity index 100% rename from packages/ui/permission/package.json rename to packages/interaction/permission/package.json diff --git a/packages/ui/permission/src/client.ts b/packages/interaction/permission/src/client.ts similarity index 100% rename from packages/ui/permission/src/client.ts rename to packages/interaction/permission/src/client.ts diff --git a/packages/ui/permission/src/index.ts b/packages/interaction/permission/src/index.ts similarity index 100% rename from packages/ui/permission/src/index.ts rename to packages/interaction/permission/src/index.ts diff --git a/packages/ui/permission/src/invariant.ts b/packages/interaction/permission/src/invariant.ts similarity index 100% rename from packages/ui/permission/src/invariant.ts rename to packages/interaction/permission/src/invariant.ts diff --git a/packages/ui/permission/src/types.ts b/packages/interaction/permission/src/types.ts similarity index 100% rename from packages/ui/permission/src/types.ts rename to packages/interaction/permission/src/types.ts diff --git a/packages/ui/permission/tests/invariant.spec.ts b/packages/interaction/permission/tests/invariant.spec.ts similarity index 100% rename from packages/ui/permission/tests/invariant.spec.ts rename to packages/interaction/permission/tests/invariant.spec.ts diff --git a/packages/ui/permission/tests/permission.spec.ts b/packages/interaction/permission/tests/permission.spec.ts similarity index 100% rename from packages/ui/permission/tests/permission.spec.ts rename to packages/interaction/permission/tests/permission.spec.ts diff --git a/packages/ui/permission/tests/projection.spec.ts b/packages/interaction/permission/tests/projection.spec.ts similarity index 100% rename from packages/ui/permission/tests/projection.spec.ts rename to packages/interaction/permission/tests/projection.spec.ts diff --git a/packages/ui/permission/tsconfig.json b/packages/interaction/permission/tsconfig.json similarity index 92% rename from packages/ui/permission/tsconfig.json rename to packages/interaction/permission/tsconfig.json index 9dc4afcd9a..b951d622a8 100644 --- a/packages/ui/permission/tsconfig.json +++ b/packages/interaction/permission/tsconfig.json @@ -36,7 +36,7 @@ "path": "../../support/invariants" }, { - "path": "../../session-projection/session-projection" + "path": "../../session/session-projection" }, { "path": "../../settings/settings" diff --git a/packages/ui/tool-ask-user/README.i18n.yaml b/packages/interaction/tool-ask-user/README.i18n.yaml similarity index 79% rename from packages/ui/tool-ask-user/README.i18n.yaml rename to packages/interaction/tool-ask-user/README.i18n.yaml index bbd0ba0561..f88c303525 100644 --- a/packages/ui/tool-ask-user/README.i18n.yaml +++ b/packages/interaction/tool-ask-user/README.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 packages/ui/tool-ask-user/README.md +# pnpm run verify-translation-pairing --write packages/interaction/tool-ask-user/README.md README.md: 7af356263ea6582a081e7c6de22fd317ca8b96df README.zh.md: 3f7b814b83c8c6957a4b2574ee69e87d45f65ae6 diff --git a/packages/ui/tool-ask-user/README.md b/packages/interaction/tool-ask-user/README.md similarity index 100% rename from packages/ui/tool-ask-user/README.md rename to packages/interaction/tool-ask-user/README.md diff --git a/packages/ui/tool-ask-user/README.zh.md b/packages/interaction/tool-ask-user/README.zh.md similarity index 100% rename from packages/ui/tool-ask-user/README.zh.md rename to packages/interaction/tool-ask-user/README.zh.md diff --git a/packages/ui/tool-ask-user/package.json b/packages/interaction/tool-ask-user/package.json similarity index 100% rename from packages/ui/tool-ask-user/package.json rename to packages/interaction/tool-ask-user/package.json diff --git a/packages/ui/tool-ask-user/src/index.ts b/packages/interaction/tool-ask-user/src/index.ts similarity index 100% rename from packages/ui/tool-ask-user/src/index.ts rename to packages/interaction/tool-ask-user/src/index.ts diff --git a/packages/ui/tool-ask-user/src/invariant.ts b/packages/interaction/tool-ask-user/src/invariant.ts similarity index 100% rename from packages/ui/tool-ask-user/src/invariant.ts rename to packages/interaction/tool-ask-user/src/invariant.ts diff --git a/packages/ui/tool-ask-user/tests/tool-ask-user.spec.ts b/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts similarity index 100% rename from packages/ui/tool-ask-user/tests/tool-ask-user.spec.ts rename to packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts diff --git a/packages/ui/tool-ask-user/tsconfig.json b/packages/interaction/tool-ask-user/tsconfig.json similarity index 100% rename from packages/ui/tool-ask-user/tsconfig.json rename to packages/interaction/tool-ask-user/tsconfig.json diff --git a/packages/cordis/tool-cordis/README.i18n.yaml b/packages/interaction/user-approval/README.i18n.yaml similarity index 54% rename from packages/cordis/tool-cordis/README.i18n.yaml rename to packages/interaction/user-approval/README.i18n.yaml index fd2b5443ef..2122c0d101 100644 --- a/packages/cordis/tool-cordis/README.i18n.yaml +++ b/packages/interaction/user-approval/README.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 packages/cordis/tool-cordis/README.md -README.md: 9986310160c2b56126155a4c3ef84d66018d31c2 -README.zh.md: d955306e1e5d4154f58c771704782ece44a15c99 +# pnpm run verify-translation-pairing --write packages/interaction/user-approval/README.md +README.md: 0cf5d458863194e29f8c84168a6f089baabbf3d2 +README.zh.md: a48e4ff4650fbd90744c9d7c41878b6e0487127c diff --git a/packages/ui/user-approval/README.md b/packages/interaction/user-approval/README.md similarity index 97% rename from packages/ui/user-approval/README.md rename to packages/interaction/user-approval/README.md index 7b87a75d1c..0cf5d45886 100644 --- a/packages/ui/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/ui/user-approval/README.zh.md b/packages/interaction/user-approval/README.zh.md similarity index 96% rename from packages/ui/user-approval/README.zh.md rename to packages/interaction/user-approval/README.zh.md index 3c02f8d673..a48e4ff465 100644 --- a/packages/ui/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/ui/user-approval/package.json b/packages/interaction/user-approval/package.json similarity index 100% rename from packages/ui/user-approval/package.json rename to packages/interaction/user-approval/package.json diff --git a/packages/ui/user-approval/src/index.ts b/packages/interaction/user-approval/src/index.ts similarity index 100% rename from packages/ui/user-approval/src/index.ts rename to packages/interaction/user-approval/src/index.ts diff --git a/packages/ui/user-approval/src/invariant.ts b/packages/interaction/user-approval/src/invariant.ts similarity index 100% rename from packages/ui/user-approval/src/invariant.ts rename to packages/interaction/user-approval/src/invariant.ts diff --git a/packages/ui/user-approval/src/types.ts b/packages/interaction/user-approval/src/types.ts similarity index 100% rename from packages/ui/user-approval/src/types.ts rename to packages/interaction/user-approval/src/types.ts diff --git a/packages/ui/user-approval/tests/approval.spec.ts b/packages/interaction/user-approval/tests/approval.spec.ts similarity index 100% rename from packages/ui/user-approval/tests/approval.spec.ts rename to packages/interaction/user-approval/tests/approval.spec.ts diff --git a/packages/ui/user-approval/tests/invariant.spec.ts b/packages/interaction/user-approval/tests/invariant.spec.ts similarity index 100% rename from packages/ui/user-approval/tests/invariant.spec.ts rename to packages/interaction/user-approval/tests/invariant.spec.ts diff --git a/packages/ui/user-approval/tsconfig.json b/packages/interaction/user-approval/tsconfig.json similarity index 100% rename from packages/ui/user-approval/tsconfig.json rename to packages/interaction/user-approval/tsconfig.json diff --git a/packages/ui/user-approval/tsdown.config.ts b/packages/interaction/user-approval/tsdown.config.ts similarity index 100% rename from packages/ui/user-approval/tsdown.config.ts rename to packages/interaction/user-approval/tsdown.config.ts diff --git a/packages/ui/user-interaction/README.i18n.yaml b/packages/interaction/user-interaction/README.i18n.yaml similarity index 78% rename from packages/ui/user-interaction/README.i18n.yaml rename to packages/interaction/user-interaction/README.i18n.yaml index 1da849ed9b..aafe594c7e 100644 --- a/packages/ui/user-interaction/README.i18n.yaml +++ b/packages/interaction/user-interaction/README.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 packages/ui/user-interaction/README.md +# pnpm run verify-translation-pairing --write packages/interaction/user-interaction/README.md README.md: cba015e782623b3a5adf018303823577a0b96774 README.zh.md: a5f944850c5ac4e05da59e8478167eef91796610 diff --git a/packages/ui/user-interaction/README.md b/packages/interaction/user-interaction/README.md similarity index 100% rename from packages/ui/user-interaction/README.md rename to packages/interaction/user-interaction/README.md diff --git a/packages/ui/user-interaction/README.zh.md b/packages/interaction/user-interaction/README.zh.md similarity index 100% rename from packages/ui/user-interaction/README.zh.md rename to packages/interaction/user-interaction/README.zh.md diff --git a/packages/ui/user-interaction/package.json b/packages/interaction/user-interaction/package.json similarity index 100% rename from packages/ui/user-interaction/package.json rename to packages/interaction/user-interaction/package.json diff --git a/packages/ui/user-interaction/src/index.ts b/packages/interaction/user-interaction/src/index.ts similarity index 100% rename from packages/ui/user-interaction/src/index.ts rename to packages/interaction/user-interaction/src/index.ts diff --git a/packages/ui/user-interaction/src/invariant.ts b/packages/interaction/user-interaction/src/invariant.ts similarity index 100% rename from packages/ui/user-interaction/src/invariant.ts rename to packages/interaction/user-interaction/src/invariant.ts diff --git a/packages/ui/user-interaction/src/types.ts b/packages/interaction/user-interaction/src/types.ts similarity index 100% rename from packages/ui/user-interaction/src/types.ts rename to packages/interaction/user-interaction/src/types.ts diff --git a/packages/ui/user-interaction/tests/user-interaction.spec.ts b/packages/interaction/user-interaction/tests/user-interaction.spec.ts similarity index 100% rename from packages/ui/user-interaction/tests/user-interaction.spec.ts rename to packages/interaction/user-interaction/tests/user-interaction.spec.ts diff --git a/packages/ui/user-interaction/tsconfig.json b/packages/interaction/user-interaction/tsconfig.json similarity index 100% rename from packages/ui/user-interaction/tsconfig.json rename to packages/interaction/user-interaction/tsconfig.json diff --git a/packages/llm/README.i18n.yaml b/packages/llm/README.i18n.yaml index 3c4ae19900..a35bf870d4 100644 --- a/packages/llm/README.i18n.yaml +++ b/packages/llm/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/llm/README.md -README.md: 92d9fbfa2b8c8db4700562009db49229b2189ab3 -README.zh.md: 5c6e7aad1db6511bdb660b86e257652128db131f +README.md: 077ae600c89cff80cd71830e37bda00707c1d837 +README.zh.md: e2bdad7fd0b33116498b50bfa6527974fa140fc9 diff --git a/packages/llm/README.md b/packages/llm/README.md index 92d9fbfa2b..077ae600c8 100644 --- a/packages/llm/README.md +++ b/packages/llm/README.md @@ -13,3 +13,5 @@ The LLM seam and its provider adapters. The interface package (`llm`) owns the a | [`llm-pi-ai/`](llm-pi-ai/README.md) | Multi-provider pi-ai adapter | registers on `ctx.llm` | Adapters register provider routes on the seam; retry and token measurement remain separate consumers. The child READMEs own routing, metadata, replay, and provider-wire details; the [LLM architecture decisions](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) own the rationale. + +The subsystem reference — messages and blocks, the model request, the `StreamChunk` protocol, the adapter contract — is [docs/subsystems/llm-streaming.md](../../docs/subsystems/llm-streaming.md) (token measurement: [token-meter.md](../../docs/subsystems/token-meter.md)); see the [twin adapters](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md), [replay token meter](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md), and [routed model context](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md) Agent Notes. diff --git a/packages/llm/README.zh.md b/packages/llm/README.zh.md index 5c6e7aad1d..e2bdad7fd0 100644 --- a/packages/llm/README.zh.md +++ b/packages/llm/README.zh.md @@ -13,3 +13,5 @@ LLM(大语言模型)seam 及其提供方适配器。接口包(`llm`)负 | [`llm-pi-ai/`](llm-pi-ai/README.md) | 多提供方 pi-ai 适配器 | 注册到 `ctx.llm` | 适配器在 seam 上注册提供方路由;重试与 token 测量仍是独立消费方。子 README 负责路由、元数据、回放和提供方协议细节;[LLM 架构决策](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md)负责设计原理。 + +子系统参考——消息与内容块、模型请求、`StreamChunk` 协议、适配器契约——见 [docs/subsystems/llm-streaming.md](../../docs/subsystems/llm-streaming.md)(token 计量:[token-meter.md](../../docs/subsystems/token-meter.md));另见[孪生适配器](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md)、[回放 token 计量](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md)与[按路由模型上下文](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md) Agent Note。 diff --git a/packages/llm/token-meter/tsconfig.json b/packages/llm/token-meter/tsconfig.json index b8b32e36fa..404607f87a 100644 --- a/packages/llm/token-meter/tsconfig.json +++ b/packages/llm/token-meter/tsconfig.json @@ -27,7 +27,7 @@ "path": "../../compact/compact" }, { - "path": "../../session-projection/session-projection" + "path": "../../session/session-projection" }, { "path": "../../support/invariants" diff --git a/packages/lsp/README.i18n.yaml b/packages/lsp/README.i18n.yaml index 9919cdaba5..be1dc838ff 100644 --- a/packages/lsp/README.i18n.yaml +++ b/packages/lsp/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/lsp/README.md -README.md: 7fbdf071735673fb0158f6fa66148be1c644a433 -README.zh.md: e059dbd80b7e38c0e447e54178162316dfd127c7 +README.md: 01c464c9993571990e0542df9fadb2848a6c9a79 +README.zh.md: fa7dddeb011bd34c1f9cbe3a9dbf746d7f163897 diff --git a/packages/lsp/README.md b/packages/lsp/README.md index 7fbdf07173..01c464c999 100644 --- a/packages/lsp/README.md +++ b/packages/lsp/README.md @@ -13,3 +13,5 @@ The language-server capability seam: an abstract LSP interface, a generic stdio The interface lives at `lsp/lsp/`. The seam exposes exactly four semantic operations — `goToDefinition`, `findReferences`, `goToImplementation`, `hover` — and no generic JSON-RPC escape hatch, so a provider swap does not change how the model asks for navigation and no protocol payload or unreviewed mutation reaches the model contract. Providers register **capabilities**, not tools; `tool-lsp` is the only owner of the model-facing name, schema, prompt guidance, and presentation. See the [LSP capability seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md) for the design rationale, including why documents open transiently per query, why the stdio host consumes the shared filesystem/subprocess execution world, and why extension ownership is exclusive within one runtime. + +The subsystem reference — operations, coordinates, requests/results, `LspError` — is [docs/subsystems/lsp.md](../../docs/subsystems/lsp.md); design rationale in the [LSP capability seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md). diff --git a/packages/lsp/README.zh.md b/packages/lsp/README.zh.md index e059dbd80b..fa7dddeb01 100644 --- a/packages/lsp/README.zh.md +++ b/packages/lsp/README.zh.md @@ -13,3 +13,5 @@ 接口位于 `lsp/lsp/`。该 seam 恰好公开四种语义操作:`goToDefinition`、`findReferences`、`goToImplementation`、`hover`,且不提供通用 JSON-RPC 逃生口;因此,替换提供方不会改变模型请求导航的方式,也不会让协议载荷或未经评审的修改进入模型契约。提供方注册的是**能力** 而非工具;`tool-lsp` 是面向模型名称、schema、提示词指引和呈现的唯一 owner。 设计原理见 [LSP 能力 seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md),其中也解释了文档为何在每次查询时临时打开、stdio 主机为何使用共享的文件系统/子进程执行环境,以及扩展名归属为何在同一运行时内互斥。 + +子系统参考——操作、坐标、请求/结果、`LspError`——见 [docs/subsystems/lsp.md](../../docs/subsystems/lsp.md);设计依据见 [LSP 能力 seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md)。 diff --git a/packages/plan/README.i18n.yaml b/packages/plan/README.i18n.yaml index 3ae6755be6..4e34bac80c 100644 --- a/packages/plan/README.i18n.yaml +++ b/packages/plan/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/plan/README.md -README.md: 598974e105aaeaf3c35418aec0655de2a5ba6888 -README.zh.md: 0f54299af5d34555d86d82e83b92d185045e770f +README.md: 535a2b02e297d7c3bc68538fa1d03226089372e0 +README.zh.md: f068bf37b44daa0d724fbb85ad85a4d62883e847 diff --git a/packages/plan/README.md b/packages/plan/README.md index 598974e105..535a2b02e2 100644 --- a/packages/plan/README.md +++ b/packages/plan/README.md @@ -9,3 +9,5 @@ Plan mode is logged, per-agent collaboration state rather than a generic mode re | [`plan-mode/`](plan-mode/README.md) | Owns plan-mode state, guidance, commands, and review flow | `ctx.planMode` | The [plan-specific collaboration state](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md) decision records the family design. + +The subsystem reference — the `plan/mode` fold, the step-boundary flush, configuration, the exit tool — is [docs/subsystems/plan.md](../../docs/subsystems/plan.md); design in [plan-specific collaboration state](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md). diff --git a/packages/plan/README.zh.md b/packages/plan/README.zh.md index 0f54299af5..f068bf37b4 100644 --- a/packages/plan/README.zh.md +++ b/packages/plan/README.zh.md @@ -9,3 +9,5 @@ Plan mode 是按 agent(智能体)记录的协作状态,而不是通用模 | [`plan-mode/`](plan-mode/README.md) | 负责 plan mode 状态、指引、命令和评审流程 | `ctx.planMode` | [plan 专用协作状态](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md)决策记录了该家族的设计。 + +子系统参考——`plan/mode` 折叠、步边界冲刷、配置、退出工具——见 [docs/subsystems/plan.md](../../docs/subsystems/plan.md);设计见[计划专属协作状态](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md)。 diff --git a/packages/plan/plan-mode/README.i18n.yaml b/packages/plan/plan-mode/README.i18n.yaml index b24d4b1954..86afdb6310 100644 --- a/packages/plan/plan-mode/README.i18n.yaml +++ b/packages/plan/plan-mode/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/plan/plan-mode/README.md -README.md: f2a78fe98b6a85ce91d727e95ddf62e6f77546b7 -README.zh.md: e1b5f111fd8f761fdd8f25558463c0cc327a14be +README.md: 6c8ba23b76e83665d4f8dcb5ecb41689347f6423 +README.zh.md: b306d5562af21dd9a95bb68c9acfc3f40c52136f diff --git a/packages/plan/plan-mode/README.md b/packages/plan/plan-mode/README.md index f2a78fe98b..6c8ba23b76 100644 --- a/packages/plan/plan-mode/README.md +++ b/packages/plan/plan-mode/README.md @@ -22,7 +22,7 @@ The Web client consumes the plugin-owned `/plan` command; other front doors may ## Session projection -When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.md)), this package registers the `plan` projection unit under an injected child. The unit folds two event kinds: a `command/run` record named `plan` with recorded `args` sets the wanted target (`off` → inactive, anything else → active), and `plan/mode` commits the logged state and clears it; every other event returns the same state reference. `view` derives `{ active, pending }`, where `pending` is true only while an outstanding selection differs from the logged state — a pure replay quantity, so host restarts, other tabs, and cold reads all recover it from the log alone (the `/plan` handler calls `set()` before any failing path, keeping the logged request and the run plane from forking). The key merges into `SessionProjectionMap` from `src/types.ts` (served to host consumers via `./types` and client aggregates via `./client`); the framework drives the unit and carriers serve the value on the history tail page and the `session/projection` push frame. Compositions without the registry are unaffected. +When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md)), this package registers the `plan` projection unit under an injected child. The unit folds two event kinds: a `command/run` record named `plan` with recorded `args` sets the wanted target (`off` → inactive, anything else → active), and `plan/mode` commits the logged state and clears it; every other event returns the same state reference. `view` derives `{ active, pending }`, where `pending` is true only while an outstanding selection differs from the logged state — a pure replay quantity, so host restarts, other tabs, and cold reads all recover it from the log alone (the `/plan` handler calls `set()` before any failing path, keeping the logged request and the run plane from forking). The key merges into `SessionProjectionMap` from `src/types.ts` (served to host consumers via `./types` and client aggregates via `./client`); the framework drives the unit and carriers serve the value on the history tail page and the `session/projection` push frame. Compositions without the registry are unaffected. ## Configuration diff --git a/packages/plan/plan-mode/README.zh.md b/packages/plan/plan-mode/README.zh.md index e1b5f111fd..b306d5562a 100644 --- a/packages/plan/plan-mode/README.zh.md +++ b/packages/plan/plan-mode/README.zh.md @@ -22,7 +22,7 @@ Web 客户端使用该插件提供的 `/plan` 命令;其他入口可以直接 ## 会话投影 -当组合挂载 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.md))时,本包会在一个注入的子插件中注册 `plan` 投影单元。该单元折叠两类事件:名为 `plan` 且携带已记录 `args` 的 `command/run` 记录会设置目标状态(`off` → 未激活,其余 → 激活),`plan/mode` 会提交已记录状态并清除该目标;其他任何事件都返回同一个状态引用。`view` 推导 `{ active, pending }`,其中 `pending` 仅在尚未落实的选择与已记录状态不同时为 true。该值完全由日志回放得出,因此 host 重启、其他标签页和冷读都能仅凭日志恢复它。`/plan` 处理器会在任何可能失败的路径之前调用 `set()`,避免已写入日志的请求与运行面分叉。key 由 `src/types.ts` 通过声明合并加入 `SessionProjectionMap`:host 消费方经 `./types` 获取,client 聚合经 `./client` 获取。框架负责驱动该单元,载体通过历史尾页和 `session/projection` 推送帧提供其值。未挂载注册表的组合不受影响。 +当组合挂载 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md))时,本包会在一个注入的子插件中注册 `plan` 投影单元。该单元折叠两类事件:名为 `plan` 且携带已记录 `args` 的 `command/run` 记录会设置目标状态(`off` → 未激活,其余 → 激活),`plan/mode` 会提交已记录状态并清除该目标;其他任何事件都返回同一个状态引用。`view` 推导 `{ active, pending }`,其中 `pending` 仅在尚未落实的选择与已记录状态不同时为 true。该值完全由日志回放得出,因此 host 重启、其他标签页和冷读都能仅凭日志恢复它。`/plan` 处理器会在任何可能失败的路径之前调用 `set()`,避免已写入日志的请求与运行面分叉。key 由 `src/types.ts` 通过声明合并加入 `SessionProjectionMap`:host 消费方经 `./types` 获取,client 聚合经 `./client` 获取。框架负责驱动该单元,载体通过历史尾页和 `session/projection` 推送帧提供其值。未挂载注册表的组合不受影响。 ## 配置 diff --git a/packages/plan/plan-mode/tsconfig.json b/packages/plan/plan-mode/tsconfig.json index a9b2d4dccf..4dde8fda45 100644 --- a/packages/plan/plan-mode/tsconfig.json +++ b/packages/plan/plan-mode/tsconfig.json @@ -27,13 +27,13 @@ "path": "../../core/system-prompt" }, { - "path": "../../ui/user-interaction" + "path": "../../interaction/user-interaction" }, { - "path": "../../ui/commands" + "path": "../../interaction/commands" }, { - "path": "../../session-projection/session-projection" + "path": "../../session/session-projection" }, { "path": "../../support/invariants" diff --git a/packages/pty/README.i18n.yaml b/packages/pty/README.i18n.yaml index 65d541a7c1..7f4c7f1f21 100644 --- a/packages/pty/README.i18n.yaml +++ b/packages/pty/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/pty/README.md -README.md: a4f743056b4a524be9623b0f700f37e0534b463f -README.zh.md: c84ad3f1b59afcdbbd111f1b82c57c56aa24fdcf +README.md: 2afb3cd61ef4bfba460716549d41f799e419ff46 +README.zh.md: 57a5c93047ccb970f499f02c0168db7e9f71b622 diff --git a/packages/pty/README.md b/packages/pty/README.md index a4f743056b..2afb3cd61e 100644 --- a/packages/pty/README.md +++ b/packages/pty/README.md @@ -11,3 +11,5 @@ English | [中文](README.zh.md) | `tool-pty` (`@deepseek-ai/dsh-tool-pty`) | Six model-facing tools and generic task integration for background sends | registers on `ctx.tools` | The design and deferred boundaries live in the [persistent PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md). + +The subsystem reference — ids, backend/session contracts, send readiness, bounded reads — is [docs/subsystems/pty.md](../../docs/subsystems/pty.md); design and deferred boundaries in the [persistent PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md). diff --git a/packages/pty/README.zh.md b/packages/pty/README.zh.md index c84ad3f1b5..57a5c93047 100644 --- a/packages/pty/README.zh.md +++ b/packages/pty/README.zh.md @@ -11,3 +11,5 @@ | `tool-pty`(`@deepseek-ai/dsh-tool-pty`) | 6 个面向模型的工具,并为后台发送集成通用任务 | 注册到 `ctx.tools` | 设计与暂缓边界记录在[持久 PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md) 中。 + +子系统参考——id、后端/会话契约、发送就绪、有界读取——见 [docs/subsystems/pty.md](../../docs/subsystems/pty.md);设计与延后边界见[持久 PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md)。 diff --git a/packages/sandbox/README.i18n.yaml b/packages/sandbox/README.i18n.yaml index c851d420ae..ab79d37170 100644 --- a/packages/sandbox/README.i18n.yaml +++ b/packages/sandbox/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/sandbox/README.md -README.md: 1ba56d22330c26302c9283b2319d983ebf902219 -README.zh.md: 5daf830854297fe14c6560dbcde8a380ad079a0e +README.md: 61127118c895cce728ab0b4eed387f51054a83f1 +README.zh.md: 984a677aad6ab97579abf861ea8b99f149c07d08 diff --git a/packages/sandbox/README.md b/packages/sandbox/README.md index 1ba56d2233..61127118c8 100644 --- a/packages/sandbox/README.md +++ b/packages/sandbox/README.md @@ -11,3 +11,5 @@ This family applies per-session confinement policy to process execution. It cove | [`sandbox-policy/`](sandbox-policy/README.md) | Resolves durable per-session sandbox policy | `ctx.sandboxPolicy` | See the [sandbox decision](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) for the capability boundary and the [filesystem integration decision](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) for cross-family policy use. + +The subsystem reference — modes and enforcement, per-call policy, wrapped-argv dialects, fail-closed errors — is [docs/subsystems/sandbox.md](../../docs/subsystems/sandbox.md); the boundary and the cross-family phase live in the [sandbox](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) and [cross-family fs sandbox](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) Agent Notes. diff --git a/packages/sandbox/README.zh.md b/packages/sandbox/README.zh.md index 5daf830854..984a677aad 100644 --- a/packages/sandbox/README.zh.md +++ b/packages/sandbox/README.zh.md @@ -11,3 +11,5 @@ | [`sandbox-policy/`](sandbox-policy/README.md) | 解析持久的逐会话沙箱策略 | `ctx.sandboxPolicy` | [沙箱决策](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)记录了能力边界,[文件系统集成决策](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)记录了跨家族策略的使用方式。 + +子系统参考——模式与强制执行、按调用策略、包装 argv 方言、故障关闭错误——见 [docs/subsystems/sandbox.md](../../docs/subsystems/sandbox.md);边界与跨家族阶段见[沙箱](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)与[跨家族 fs 沙箱](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) Agent Note。 diff --git a/packages/sandbox/sandbox/README.i18n.yaml b/packages/sandbox/sandbox/README.i18n.yaml index 11f2a2ed86..99a5265418 100644 --- a/packages/sandbox/sandbox/README.i18n.yaml +++ b/packages/sandbox/sandbox/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/sandbox/sandbox/README.md -README.md: 1b522b2c72d00bfed89650aa7f22b65a72d26085 -README.zh.md: adccd4421a74ef073ad3ffc3a23bccb0354d99aa +README.md: 8c1c747f6c7178f9c8c1a9827675ffa98f29aa6d +README.zh.md: e43d1918a99fa96688632551a8f861c2c91f9c2a diff --git a/packages/sandbox/sandbox/README.md b/packages/sandbox/sandbox/README.md index 1b522b2c72..8c1c747f6c 100644 --- a/packages/sandbox/sandbox/README.md +++ b/packages/sandbox/sandbox/README.md @@ -4,7 +4,7 @@ English | [中文](README.zh.md) Abstract process-sandbox seam. Owns the `ctx.sandbox` service contract ([`SandboxProvider`](src/index.ts)) and the confinement vocabulary the harness shares: `SandboxMode` (`read-only` / `workspace-write` / `danger-full-access`, file effects only), `SandboxEnforcement` (`full` / `partial`, per kernel ABI), `SandboxExecutionPolicy` (the complete per-call mode + workspace root), `SandboxPolicy` (its confined subset), and the fail-closed `SANDBOX_UNAVAILABLE` error. Interface package of the [capability-seam split](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): depends only on cordis (+ the harness error base), never on a backend. -The contract in one line: `ctx.sandbox.confine(argv, policy)` returns the argv to spawn INSTEAD of your own — wrapped so the process (and everything it spawns) runs confined — plus the selected backend's enforcement completeness, denial dialect (`denialSignatures`), and structured runner-failure evidence (`runnerFailureRules`); when no backend is usable it throws rather than passing the argv through unconfined. The [core type catalog](../../../docs/core-data-structures/sandbox.md#wrapped-argv-and-classification-dialects) owns the exact classifier shape. +The contract in one line: `ctx.sandbox.confine(argv, policy)` returns the argv to spawn INSTEAD of your own — wrapped so the process (and everything it spawns) runs confined — plus the selected backend's enforcement completeness, denial dialect (`denialSignatures`), and structured runner-failure evidence (`runnerFailureRules`); when no backend is usable it throws rather than passing the argv through unconfined. The [core type catalog](../../../docs/subsystems/sandbox.md#wrapped-argv-and-classification-dialects) owns the exact classifier shape. Policy rides the call, not the provider: two consumers may confine under different policies at the same instant (bash under `read-only` while a confined child agent keeps its state directory writable), and an approved escalated retry is just a new call with a wider policy. diff --git a/packages/sandbox/sandbox/README.zh.md b/packages/sandbox/sandbox/README.zh.md index adccd4421a..e43d1918a9 100644 --- a/packages/sandbox/sandbox/README.zh.md +++ b/packages/sandbox/sandbox/README.zh.md @@ -4,7 +4,7 @@ 抽象进程沙箱 seam。负责定义 `ctx.sandbox` 服务契约([`SandboxProvider`](src/index.ts))与 harness 共享的限制词汇:`SandboxMode`(`read-only`/`workspace-write`/`danger-full-access`,仅限文件操作)、`SandboxEnforcement`(`full`/`partial`,针对每种内核 ABI)、`SandboxExecutionPolicy`(每次调用的完整模式及工作区根目录)、`SandboxPolicy`(其中受限制的子集),以及故障时拒绝放行的 `SANDBOX_UNAVAILABLE` 错误。它是[能力 seam 拆分](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)的接口包:只依赖 cordis(及 harness 错误基类),绝不依赖后端。 -用一句话概括契约:`ctx.sandbox.confine(argv, policy)` 返回用于 spawn、应当取代调用方原始 argv 的 argv。返回值经过包装,使进程及其派生的所有进程都在限制下运行;还会附带所选后端达到的强制执行完整度、拒绝方言(`denialSignatures`)和结构化 runner 失败证据(`runnerFailureRules`)。没有可用后端时,它会抛出异常,绝不会原样传递 argv 使其不受限制地运行。[核心类型目录](../../../docs/core-data-structures/sandbox.md#wrapped-argv-and-classification-dialects)负责定义分类器的精确结构。 +用一句话概括契约:`ctx.sandbox.confine(argv, policy)` 返回用于 spawn、应当取代调用方原始 argv 的 argv。返回值经过包装,使进程及其派生的所有进程都在限制下运行;还会附带所选后端达到的强制执行完整度、拒绝方言(`denialSignatures`)和结构化 runner 失败证据(`runnerFailureRules`)。没有可用后端时,它会抛出异常,绝不会原样传递 argv 使其不受限制地运行。[核心类型目录](../../../docs/subsystems/sandbox.md#wrapped-argv-and-classification-dialects)负责定义分类器的精确结构。 策略随调用传递,而不属于提供方:两个消费方可以同时按不同策略施加限制(bash 使用 `read-only`,而受限制的子 agent(智能体)保持其状态目录可写);获批的升权重试只是使用更宽策略发起的新调用。 diff --git a/packages/scaffold/README.i18n.yaml b/packages/scaffold/README.i18n.yaml new file mode 100644 index 0000000000..7ddc22328c --- /dev/null +++ b/packages/scaffold/README.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 packages/scaffold/README.md +README.md: 19c2f28f48d1c2d8747b635c10d6781c29e02f15 +README.zh.md: ecad2592eb32a30444f9112571379939da6afeec diff --git a/packages/sdk/README.md b/packages/scaffold/README.md similarity index 64% rename from packages/sdk/README.md rename to packages/scaffold/README.md index 3f99d6d45d..19c2f28f48 100644 --- a/packages/sdk/README.md +++ b/packages/scaffold/README.md @@ -1,16 +1,17 @@ -# SDK packages +# scaffold/ — create, launch, and drive projects from outside English | [中文](README.zh.md) -This group contains developer tooling for Harness projects and the client stack for driving a Harness runtime from another process. +This group contains developer tooling for Harness projects and the client stack for driving a Harness runtime from another process. Folders are role-named without the legacy `sdk-` prefix; npm names converge on `dsh-sdk-*` through the FIXME-tracked renames in the [regrouping Agent Note](../../.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md). | Package | Role | |---|---| | [`helper/`](helper/README.md) | Provides the shared project-editing domain | | [`scripts/`](scripts/README.md) | Provides the `dsh-sdk` project commands | | [`create-sdk/`](create-sdk/README.md) | Creates new SDK projects | -| [`sdk-protocol/`](sdk-protocol/README.md) | Defines the SDK runtime wire protocol | -| [`sdk-client/`](sdk-client/README.md) | Drives a Harness runtime through the TypeScript client API | +| [`protocol/`](protocol/README.md) | Defines the SDK runtime wire protocol | +| [`client/`](client/README.md) | Drives a Harness runtime through the TypeScript client API | +| [`server/`](server/README.md) | Serves out-of-process SDK clients over stdio JSON-RPC | | [`telemetry/`](telemetry/README.md) | Provides launcher telemetry, consent, and redaction primitives | `@deepseek-ai/create-sdk` follows npm's scoped initializer naming convention; the other packages follow the repository's `@deepseek-ai/dsh-*` convention. See the [developer-project workflow](../../.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md), [project-editing architecture](../../.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md), and [TypeScript SDK design](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md). diff --git a/packages/scaffold/README.zh.md b/packages/scaffold/README.zh.md new file mode 100644 index 0000000000..ecad2592eb --- /dev/null +++ b/packages/scaffold/README.zh.md @@ -0,0 +1,17 @@ +# scaffold/:从外部创建、启动、驱动项目 + +[English](README.md) | 中文 + +本组包含 Harness 项目的开发者工具,以及从另一进程驱动 Harness 运行时的客户端栈。目录按角色命名,不带遗留的 `sdk-` 前缀;npm 名则经由[重新分组 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md)中 FIXME 跟踪的改名收敛为 `dsh-sdk-*`。 + +| 包 | 职责 | +|---|---| +| [`helper/`](helper/README.md) | 提供共享的项目编辑领域 | +| [`scripts/`](scripts/README.md) | 提供 `dsh-sdk` 项目命令 | +| [`create-sdk/`](create-sdk/README.md) | 创建新的 SDK 项目 | +| [`protocol/`](protocol/README.md) | 定义 SDK 运行时通信协议 | +| [`client/`](client/README.md) | 通过 TypeScript 客户端 API 驱动 Harness 运行时 | +| [`server/`](server/README.md) | 通过 stdio JSON-RPC 为进程外 SDK 客户端提供服务 | +| [`telemetry/`](telemetry/README.md) | 提供启动器 telemetry、同意与脱敏原语 | + +`@deepseek-ai/create-sdk` 遵循 npm 的 scoped initializer 命名约定;其余包遵循仓库的 `@deepseek-ai/dsh-*` 约定。参见[开发者项目工作流](../../.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md)、[项目编辑架构](../../.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md)与 [TypeScript SDK 设计](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md)。 diff --git a/packages/scaffold/client/README.i18n.yaml b/packages/scaffold/client/README.i18n.yaml new file mode 100644 index 0000000000..c08016649d --- /dev/null +++ b/packages/scaffold/client/README.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 packages/scaffold/client/README.md +README.md: b33457875f81d11d09bab2e5aa5ce730e233c78a +README.zh.md: 271f07ffb0f97abe005971962beb517acfdc05a4 diff --git a/packages/sdk/sdk-client/README.md b/packages/scaffold/client/README.md similarity index 99% rename from packages/sdk/sdk-client/README.md rename to packages/scaffold/client/README.md index f27ec25633..b33457875f 100644 --- a/packages/sdk/sdk-client/README.md +++ b/packages/scaffold/client/README.md @@ -44,6 +44,6 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work - **No bundled-runtime resolution** — callers name the runtime executable explicitly; packaged-executable discovery stays Python-side until a TypeScript distribution consumer exists. -- **No mid-turn cancel** — the wire has no prompt-cancel method; abandoning a turn means closing the runtime (see the protocol's [Known Limitations](../sdk-protocol/README.md)). +- **No mid-turn cancel** — the wire has no prompt-cancel method; abandoning a turn means closing the runtime (see the protocol's [Known Limitations](../protocol/README.md)). - **No per-prompt result or cancel** — low-level `prompt()` returns only an enqueue receipt; high-level `run()` owns receipt-to-idle collection, and abandoning it means closing the runtime. - **Client→server notifications and server→client requests are unimplemented** on both wire ends; the transport carries them for future approval flows. diff --git a/packages/sdk/sdk-client/README.zh.md b/packages/scaffold/client/README.zh.md similarity index 99% rename from packages/sdk/sdk-client/README.zh.md rename to packages/scaffold/client/README.zh.md index bd23762500..271f07ffb0 100644 --- a/packages/sdk/sdk-client/README.zh.md +++ b/packages/scaffold/client/README.zh.md @@ -44,6 +44,6 @@ console.log(result.finalResponse) ## 已知限制与暂缓事项 - **无捆绑运行时解析**——调用方显式指定运行时可执行文件;打包可执行文件的发现留在 Python 侧,直到出现 TypeScript 发行版消费方。 -- **无轮次中取消**——协议层没有提示词取消方法;放弃轮次意味着关闭运行时(见协议的 [已知限制](../sdk-protocol/README.md))。 +- **无轮次中取消**——协议层没有提示词取消方法;放弃轮次意味着关闭运行时(见协议的 [已知限制](../protocol/README.md))。 - **没有逐提示词结果或取消**——低层 `prompt()` 只返回入队回执;高层 `run()` 负责从回执收集到 idle,放弃该过程意味着关闭运行时。 - **客户端→服务端通知与服务端→客户端请求**在协议两端都未实现;传输层为未来审批流保留了承载能力。 diff --git a/packages/sdk/sdk-client/package.json b/packages/scaffold/client/package.json similarity index 100% rename from packages/sdk/sdk-client/package.json rename to packages/scaffold/client/package.json diff --git a/packages/sdk/sdk-client/src/api.ts b/packages/scaffold/client/src/api.ts similarity index 100% rename from packages/sdk/sdk-client/src/api.ts rename to packages/scaffold/client/src/api.ts diff --git a/packages/sdk/sdk-client/src/client.ts b/packages/scaffold/client/src/client.ts similarity index 100% rename from packages/sdk/sdk-client/src/client.ts rename to packages/scaffold/client/src/client.ts diff --git a/packages/sdk/sdk-client/src/dispose.ts b/packages/scaffold/client/src/dispose.ts similarity index 100% rename from packages/sdk/sdk-client/src/dispose.ts rename to packages/scaffold/client/src/dispose.ts diff --git a/packages/sdk/sdk-client/src/index.ts b/packages/scaffold/client/src/index.ts similarity index 100% rename from packages/sdk/sdk-client/src/index.ts rename to packages/scaffold/client/src/index.ts diff --git a/packages/sdk/sdk-client/src/invariant.ts b/packages/scaffold/client/src/invariant.ts similarity index 100% rename from packages/sdk/sdk-client/src/invariant.ts rename to packages/scaffold/client/src/invariant.ts diff --git a/packages/sdk/sdk-client/src/types.ts b/packages/scaffold/client/src/types.ts similarity index 100% rename from packages/sdk/sdk-client/src/types.ts rename to packages/scaffold/client/src/types.ts diff --git a/packages/sdk/sdk-client/tests/dispose.spec.ts b/packages/scaffold/client/tests/dispose.spec.ts similarity index 100% rename from packages/sdk/sdk-client/tests/dispose.spec.ts rename to packages/scaffold/client/tests/dispose.spec.ts diff --git a/packages/sdk/sdk-client/tests/fake-runtime.ts b/packages/scaffold/client/tests/fake-runtime.ts similarity index 100% rename from packages/sdk/sdk-client/tests/fake-runtime.ts rename to packages/scaffold/client/tests/fake-runtime.ts diff --git a/packages/sdk/sdk-client/tests/sdk-client.spec.ts b/packages/scaffold/client/tests/sdk-client.spec.ts similarity index 100% rename from packages/sdk/sdk-client/tests/sdk-client.spec.ts rename to packages/scaffold/client/tests/sdk-client.spec.ts diff --git a/packages/sdk/sdk-client/tsconfig.json b/packages/scaffold/client/tsconfig.json similarity index 93% rename from packages/sdk/sdk-client/tsconfig.json rename to packages/scaffold/client/tsconfig.json index 567c6582bb..a5456f5ad3 100644 --- a/packages/sdk/sdk-client/tsconfig.json +++ b/packages/scaffold/client/tsconfig.json @@ -21,7 +21,7 @@ "path": "../../core/session" }, { - "path": "../sdk-protocol" + "path": "../protocol" }, { "path": "../../support/invariants" diff --git a/packages/sdk/create-sdk/README.i18n.yaml b/packages/scaffold/create-sdk/README.i18n.yaml similarity index 80% rename from packages/sdk/create-sdk/README.i18n.yaml rename to packages/scaffold/create-sdk/README.i18n.yaml index 27609d9a11..62d5bd3793 100644 --- a/packages/sdk/create-sdk/README.i18n.yaml +++ b/packages/scaffold/create-sdk/README.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 packages/sdk/create-sdk/README.md +# pnpm run verify-translation-pairing --write packages/scaffold/create-sdk/README.md README.md: aa09236832a50abdcd2b158e0561db26bce19cf6 README.zh.md: a4685e66bc3eb7924dd247a7b6b420f6bcc70990 diff --git a/packages/sdk/create-sdk/README.md b/packages/scaffold/create-sdk/README.md similarity index 100% rename from packages/sdk/create-sdk/README.md rename to packages/scaffold/create-sdk/README.md diff --git a/packages/sdk/create-sdk/README.zh.md b/packages/scaffold/create-sdk/README.zh.md similarity index 100% rename from packages/sdk/create-sdk/README.zh.md rename to packages/scaffold/create-sdk/README.zh.md diff --git a/packages/sdk/create-sdk/package.json b/packages/scaffold/create-sdk/package.json similarity index 100% rename from packages/sdk/create-sdk/package.json rename to packages/scaffold/create-sdk/package.json diff --git a/packages/sdk/create-sdk/src/args.ts b/packages/scaffold/create-sdk/src/args.ts similarity index 100% rename from packages/sdk/create-sdk/src/args.ts rename to packages/scaffold/create-sdk/src/args.ts diff --git a/packages/sdk/create-sdk/src/bin.ts b/packages/scaffold/create-sdk/src/bin.ts similarity index 100% rename from packages/sdk/create-sdk/src/bin.ts rename to packages/scaffold/create-sdk/src/bin.ts diff --git a/packages/sdk/create-sdk/src/command.ts b/packages/scaffold/create-sdk/src/command.ts similarity index 100% rename from packages/sdk/create-sdk/src/command.ts rename to packages/scaffold/create-sdk/src/command.ts diff --git a/packages/sdk/create-sdk/src/create-questions.ts b/packages/scaffold/create-sdk/src/create-questions.ts similarity index 100% rename from packages/sdk/create-sdk/src/create-questions.ts rename to packages/scaffold/create-sdk/src/create-questions.ts diff --git a/packages/sdk/create-sdk/src/create-wizard.ts b/packages/scaffold/create-sdk/src/create-wizard.ts similarity index 100% rename from packages/sdk/create-sdk/src/create-wizard.ts rename to packages/scaffold/create-sdk/src/create-wizard.ts diff --git a/packages/sdk/create-sdk/src/headless.ts b/packages/scaffold/create-sdk/src/headless.ts similarity index 100% rename from packages/sdk/create-sdk/src/headless.ts rename to packages/scaffold/create-sdk/src/headless.ts diff --git a/packages/sdk/create-sdk/src/index.ts b/packages/scaffold/create-sdk/src/index.ts similarity index 100% rename from packages/sdk/create-sdk/src/index.ts rename to packages/scaffold/create-sdk/src/index.ts diff --git a/packages/sdk/create-sdk/src/invariant.ts b/packages/scaffold/create-sdk/src/invariant.ts similarity index 100% rename from packages/sdk/create-sdk/src/invariant.ts rename to packages/scaffold/create-sdk/src/invariant.ts diff --git a/packages/sdk/create-sdk/src/project-scaffolder.ts b/packages/scaffold/create-sdk/src/project-scaffolder.ts similarity index 100% rename from packages/sdk/create-sdk/src/project-scaffolder.ts rename to packages/scaffold/create-sdk/src/project-scaffolder.ts diff --git a/packages/sdk/create-sdk/src/templates/assets/created.txt.tpl b/packages/scaffold/create-sdk/src/templates/assets/created.txt.tpl similarity index 100% rename from packages/sdk/create-sdk/src/templates/assets/created.txt.tpl rename to packages/scaffold/create-sdk/src/templates/assets/created.txt.tpl diff --git a/packages/sdk/create-sdk/src/templates/assets/install-question.txt.tpl b/packages/scaffold/create-sdk/src/templates/assets/install-question.txt.tpl similarity index 100% rename from packages/sdk/create-sdk/src/templates/assets/install-question.txt.tpl rename to packages/scaffold/create-sdk/src/templates/assets/install-question.txt.tpl diff --git a/packages/sdk/create-sdk/src/templates/assets/next-steps.txt.tpl b/packages/scaffold/create-sdk/src/templates/assets/next-steps.txt.tpl similarity index 100% rename from packages/sdk/create-sdk/src/templates/assets/next-steps.txt.tpl rename to packages/scaffold/create-sdk/src/templates/assets/next-steps.txt.tpl diff --git a/packages/sdk/create-sdk/src/templates/assets/setup-failure.txt.tpl b/packages/scaffold/create-sdk/src/templates/assets/setup-failure.txt.tpl similarity index 100% rename from packages/sdk/create-sdk/src/templates/assets/setup-failure.txt.tpl rename to packages/scaffold/create-sdk/src/templates/assets/setup-failure.txt.tpl diff --git a/packages/sdk/create-sdk/src/templates/assets/usage.txt.tpl b/packages/scaffold/create-sdk/src/templates/assets/usage.txt.tpl similarity index 100% rename from packages/sdk/create-sdk/src/templates/assets/usage.txt.tpl rename to packages/scaffold/create-sdk/src/templates/assets/usage.txt.tpl diff --git a/packages/sdk/create-sdk/src/templates/create-templates.ts b/packages/scaffold/create-sdk/src/templates/create-templates.ts similarity index 100% rename from packages/sdk/create-sdk/src/templates/create-templates.ts rename to packages/scaffold/create-sdk/src/templates/create-templates.ts diff --git a/packages/sdk/create-sdk/tests/built-artifacts.e2e.ts b/packages/scaffold/create-sdk/tests/built-artifacts.e2e.ts similarity index 87% rename from packages/sdk/create-sdk/tests/built-artifacts.e2e.ts rename to packages/scaffold/create-sdk/tests/built-artifacts.e2e.ts index 4b2de078c7..827abe06a2 100644 --- a/packages/sdk/create-sdk/tests/built-artifacts.e2e.ts +++ b/packages/scaffold/create-sdk/tests/built-artifacts.e2e.ts @@ -7,8 +7,8 @@ import { describe, expect, it } from 'vitest' const execFileAsync = promisify(execFile) const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url)) -const createBin = join(repoRoot, 'packages/sdk/create-sdk/lib/bin.js') -const scriptsBin = join(repoRoot, 'packages/sdk/scripts/lib/bin.js') +const createBin = join(repoRoot, 'packages/scaffold/create-sdk/lib/bin.js') +const scriptsBin = join(repoRoot, 'packages/scaffold/scripts/lib/bin.js') describe.skipIf(!existsSync(createBin) || !existsSync(scriptsBin))( 'SDK built artifacts', diff --git a/packages/sdk/create-sdk/tests/create.snapshot.ts b/packages/scaffold/create-sdk/tests/create.snapshot.ts similarity index 100% rename from packages/sdk/create-sdk/tests/create.snapshot.ts rename to packages/scaffold/create-sdk/tests/create.snapshot.ts diff --git a/packages/sdk/create-sdk/tests/create.spec.ts b/packages/scaffold/create-sdk/tests/create.spec.ts similarity index 100% rename from packages/sdk/create-sdk/tests/create.spec.ts rename to packages/scaffold/create-sdk/tests/create.spec.ts diff --git a/packages/sdk/create-sdk/tests/link-workspace.e2e.ts b/packages/scaffold/create-sdk/tests/link-workspace.e2e.ts similarity index 98% rename from packages/sdk/create-sdk/tests/link-workspace.e2e.ts rename to packages/scaffold/create-sdk/tests/link-workspace.e2e.ts index b7739acc16..b978c8217e 100644 --- a/packages/sdk/create-sdk/tests/link-workspace.e2e.ts +++ b/packages/scaffold/create-sdk/tests/link-workspace.e2e.ts @@ -17,7 +17,7 @@ import { scaffoldProject } from '../src/project-scaffolder.ts' const execFileAsync = promisify(execFile) const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url)) -const builtScripts = join(repoRoot, 'packages/sdk/scripts/lib/bin.js') +const builtScripts = join(repoRoot, 'packages/scaffold/scripts/lib/bin.js') const temporary: string[] = [] function resolveCorepackHome(): string { diff --git a/packages/sdk/create-sdk/tsconfig.json b/packages/scaffold/create-sdk/tsconfig.json similarity index 100% rename from packages/sdk/create-sdk/tsconfig.json rename to packages/scaffold/create-sdk/tsconfig.json diff --git a/packages/sdk/create-sdk/tsdown.config.ts b/packages/scaffold/create-sdk/tsdown.config.ts similarity index 100% rename from packages/sdk/create-sdk/tsdown.config.ts rename to packages/scaffold/create-sdk/tsdown.config.ts diff --git a/packages/sdk/helper/README.i18n.yaml b/packages/scaffold/helper/README.i18n.yaml similarity index 80% rename from packages/sdk/helper/README.i18n.yaml rename to packages/scaffold/helper/README.i18n.yaml index bb5acbdcc2..a62db5f38a 100644 --- a/packages/sdk/helper/README.i18n.yaml +++ b/packages/scaffold/helper/README.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 packages/sdk/helper/README.md +# pnpm run verify-translation-pairing --write packages/scaffold/helper/README.md README.md: 416fcab9815e50ca662333eb6925cc37eb0c41c4 README.zh.md: c50ec7aafa03377bd11759c50eeb2422a12fab68 diff --git a/packages/sdk/helper/README.md b/packages/scaffold/helper/README.md similarity index 100% rename from packages/sdk/helper/README.md rename to packages/scaffold/helper/README.md diff --git a/packages/sdk/helper/README.zh.md b/packages/scaffold/helper/README.zh.md similarity index 100% rename from packages/sdk/helper/README.zh.md rename to packages/scaffold/helper/README.zh.md diff --git a/packages/sdk/helper/package.json b/packages/scaffold/helper/package.json similarity index 100% rename from packages/sdk/helper/package.json rename to packages/scaffold/helper/package.json diff --git a/packages/sdk/helper/src/documents/cordis-yaml-file.ts b/packages/scaffold/helper/src/documents/cordis-yaml-file.ts similarity index 100% rename from packages/sdk/helper/src/documents/cordis-yaml-file.ts rename to packages/scaffold/helper/src/documents/cordis-yaml-file.ts diff --git a/packages/sdk/helper/src/documents/env-file.ts b/packages/scaffold/helper/src/documents/env-file.ts similarity index 100% rename from packages/sdk/helper/src/documents/env-file.ts rename to packages/scaffold/helper/src/documents/env-file.ts diff --git a/packages/sdk/helper/src/documents/package-json-file.ts b/packages/scaffold/helper/src/documents/package-json-file.ts similarity index 100% rename from packages/sdk/helper/src/documents/package-json-file.ts rename to packages/scaffold/helper/src/documents/package-json-file.ts diff --git a/packages/sdk/helper/src/documents/pnpm-workspace-file.ts b/packages/scaffold/helper/src/documents/pnpm-workspace-file.ts similarity index 100% rename from packages/sdk/helper/src/documents/pnpm-workspace-file.ts rename to packages/scaffold/helper/src/documents/pnpm-workspace-file.ts diff --git a/packages/sdk/helper/src/documents/project-file.ts b/packages/scaffold/helper/src/documents/project-file.ts similarity index 100% rename from packages/sdk/helper/src/documents/project-file.ts rename to packages/scaffold/helper/src/documents/project-file.ts diff --git a/packages/sdk/helper/src/documents/tsconfig-file.ts b/packages/scaffold/helper/src/documents/tsconfig-file.ts similarity index 100% rename from packages/sdk/helper/src/documents/tsconfig-file.ts rename to packages/scaffold/helper/src/documents/tsconfig-file.ts diff --git a/packages/sdk/helper/src/features/builtin/app.ts b/packages/scaffold/helper/src/features/builtin/app.ts similarity index 100% rename from packages/sdk/helper/src/features/builtin/app.ts rename to packages/scaffold/helper/src/features/builtin/app.ts diff --git a/packages/sdk/helper/src/features/builtin/helpers.ts b/packages/scaffold/helper/src/features/builtin/helpers.ts similarity index 100% rename from packages/sdk/helper/src/features/builtin/helpers.ts rename to packages/scaffold/helper/src/features/builtin/helpers.ts diff --git a/packages/sdk/helper/src/features/builtin/index.ts b/packages/scaffold/helper/src/features/builtin/index.ts similarity index 100% rename from packages/sdk/helper/src/features/builtin/index.ts rename to packages/scaffold/helper/src/features/builtin/index.ts diff --git a/packages/sdk/helper/src/features/builtin/provider.ts b/packages/scaffold/helper/src/features/builtin/provider.ts similarity index 100% rename from packages/sdk/helper/src/features/builtin/provider.ts rename to packages/scaffold/helper/src/features/builtin/provider.ts diff --git a/packages/sdk/helper/src/features/builtin/spine.ts b/packages/scaffold/helper/src/features/builtin/spine.ts similarity index 100% rename from packages/sdk/helper/src/features/builtin/spine.ts rename to packages/scaffold/helper/src/features/builtin/spine.ts diff --git a/packages/sdk/helper/src/features/define-feature.ts b/packages/scaffold/helper/src/features/define-feature.ts similarity index 100% rename from packages/sdk/helper/src/features/define-feature.ts rename to packages/scaffold/helper/src/features/define-feature.ts diff --git a/packages/sdk/helper/src/features/feature-configurator.ts b/packages/scaffold/helper/src/features/feature-configurator.ts similarity index 100% rename from packages/sdk/helper/src/features/feature-configurator.ts rename to packages/scaffold/helper/src/features/feature-configurator.ts diff --git a/packages/sdk/helper/src/features/feature.ts b/packages/scaffold/helper/src/features/feature.ts similarity index 100% rename from packages/sdk/helper/src/features/feature.ts rename to packages/scaffold/helper/src/features/feature.ts diff --git a/packages/sdk/helper/src/features/registry.ts b/packages/scaffold/helper/src/features/registry.ts similarity index 100% rename from packages/sdk/helper/src/features/registry.ts rename to packages/scaffold/helper/src/features/registry.ts diff --git a/packages/sdk/helper/src/features/resources.ts b/packages/scaffold/helper/src/features/resources.ts similarity index 100% rename from packages/sdk/helper/src/features/resources.ts rename to packages/scaffold/helper/src/features/resources.ts diff --git a/packages/sdk/helper/src/ids.ts b/packages/scaffold/helper/src/ids.ts similarity index 100% rename from packages/sdk/helper/src/ids.ts rename to packages/scaffold/helper/src/ids.ts diff --git a/packages/sdk/helper/src/index.ts b/packages/scaffold/helper/src/index.ts similarity index 86% rename from packages/sdk/helper/src/index.ts rename to packages/scaffold/helper/src/index.ts index 85aba58a99..db4468aae5 100644 --- a/packages/sdk/helper/src/index.ts +++ b/packages/scaffold/helper/src/index.ts @@ -1,6 +1,10 @@ /** * Shared domain and infrastructure for DeepSeek Harness SDK project tooling. * + * FIXME: rename to `@deepseek-ai/dsh-sdk-helper` before the first tagged release — + * the current name is indefensibly generic as a published name + * ([regrouping Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md)). + * * @module @deepseek-ai/dsh-helper */ diff --git a/packages/sdk/helper/src/invariant.ts b/packages/scaffold/helper/src/invariant.ts similarity index 100% rename from packages/sdk/helper/src/invariant.ts rename to packages/scaffold/helper/src/invariant.ts diff --git a/packages/sdk/helper/src/package-managers/link-workspace.ts b/packages/scaffold/helper/src/package-managers/link-workspace.ts similarity index 100% rename from packages/sdk/helper/src/package-managers/link-workspace.ts rename to packages/scaffold/helper/src/package-managers/link-workspace.ts diff --git a/packages/sdk/helper/src/package-managers/package-manager.ts b/packages/scaffold/helper/src/package-managers/package-manager.ts similarity index 100% rename from packages/sdk/helper/src/package-managers/package-manager.ts rename to packages/scaffold/helper/src/package-managers/package-manager.ts diff --git a/packages/sdk/helper/src/plugins/local-plugin-blueprint.ts b/packages/scaffold/helper/src/plugins/local-plugin-blueprint.ts similarity index 100% rename from packages/sdk/helper/src/plugins/local-plugin-blueprint.ts rename to packages/scaffold/helper/src/plugins/local-plugin-blueprint.ts diff --git a/packages/sdk/helper/src/project/change-set.ts b/packages/scaffold/helper/src/project/change-set.ts similarity index 100% rename from packages/sdk/helper/src/project/change-set.ts rename to packages/scaffold/helper/src/project/change-set.ts diff --git a/packages/sdk/helper/src/project/npm-dependency-policy.ts b/packages/scaffold/helper/src/project/npm-dependency-policy.ts similarity index 100% rename from packages/sdk/helper/src/project/npm-dependency-policy.ts rename to packages/scaffold/helper/src/project/npm-dependency-policy.ts diff --git a/packages/sdk/helper/src/project/project-edit-session.ts b/packages/scaffold/helper/src/project/project-edit-session.ts similarity index 100% rename from packages/sdk/helper/src/project/project-edit-session.ts rename to packages/scaffold/helper/src/project/project-edit-session.ts diff --git a/packages/sdk/helper/src/project/sdk-project.ts b/packages/scaffold/helper/src/project/sdk-project.ts similarity index 99% rename from packages/sdk/helper/src/project/sdk-project.ts rename to packages/scaffold/helper/src/project/sdk-project.ts index e9302aa428..41b66fe7d0 100644 --- a/packages/sdk/helper/src/project/sdk-project.ts +++ b/packages/scaffold/helper/src/project/sdk-project.ts @@ -98,7 +98,7 @@ async function detectPackageManager(root: string, manifest: Readonly): string | undefined { const spec = manifest.dependencies?.['@deepseek-ai/dsh-scripts'] - const match = /^(?:file|link|portal):(.+)\/packages\/sdk\/scripts\/?$/.exec(spec ?? '') + const match = /^(?:file|link|portal):(.+)\/packages\/scaffold\/scripts\/?$/.exec(spec ?? '') return match?.[1] ? resolve(root, match[1]) : undefined } diff --git a/packages/sdk/helper/src/project/types.ts b/packages/scaffold/helper/src/project/types.ts similarity index 100% rename from packages/sdk/helper/src/project/types.ts rename to packages/scaffold/helper/src/project/types.ts diff --git a/packages/sdk/helper/src/questions/clack-nested-multiselect.ts b/packages/scaffold/helper/src/questions/clack-nested-multiselect.ts similarity index 100% rename from packages/sdk/helper/src/questions/clack-nested-multiselect.ts rename to packages/scaffold/helper/src/questions/clack-nested-multiselect.ts diff --git a/packages/sdk/helper/src/questions/clack-prompt-port.ts b/packages/scaffold/helper/src/questions/clack-prompt-port.ts similarity index 100% rename from packages/sdk/helper/src/questions/clack-prompt-port.ts rename to packages/scaffold/helper/src/questions/clack-prompt-port.ts diff --git a/packages/sdk/helper/src/questions/headless-prompt-port.ts b/packages/scaffold/helper/src/questions/headless-prompt-port.ts similarity index 100% rename from packages/sdk/helper/src/questions/headless-prompt-port.ts rename to packages/scaffold/helper/src/questions/headless-prompt-port.ts diff --git a/packages/sdk/helper/src/questions/prompt-port.ts b/packages/scaffold/helper/src/questions/prompt-port.ts similarity index 100% rename from packages/sdk/helper/src/questions/prompt-port.ts rename to packages/scaffold/helper/src/questions/prompt-port.ts diff --git a/packages/sdk/helper/src/questions/question.ts b/packages/scaffold/helper/src/questions/question.ts similarity index 100% rename from packages/sdk/helper/src/questions/question.ts rename to packages/scaffold/helper/src/questions/question.ts diff --git a/packages/sdk/helper/src/templates/assets/README.md.tpl b/packages/scaffold/helper/src/templates/assets/README.md.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/README.md.tpl rename to packages/scaffold/helper/src/templates/assets/README.md.tpl diff --git a/packages/sdk/helper/src/templates/assets/gitignore.tpl b/packages/scaffold/helper/src/templates/assets/gitignore.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/gitignore.tpl rename to packages/scaffold/helper/src/templates/assets/gitignore.tpl diff --git a/packages/sdk/helper/src/templates/assets/index.ts.tpl b/packages/scaffold/helper/src/templates/assets/index.ts.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/index.ts.tpl rename to packages/scaffold/helper/src/templates/assets/index.ts.tpl diff --git a/packages/sdk/helper/src/templates/assets/local-plugin-tsdown.config.ts.tpl b/packages/scaffold/helper/src/templates/assets/local-plugin-tsdown.config.ts.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/local-plugin-tsdown.config.ts.tpl rename to packages/scaffold/helper/src/templates/assets/local-plugin-tsdown.config.ts.tpl diff --git a/packages/sdk/helper/src/templates/assets/local-plugin.ts.tpl b/packages/scaffold/helper/src/templates/assets/local-plugin.ts.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/local-plugin.ts.tpl rename to packages/scaffold/helper/src/templates/assets/local-plugin.ts.tpl diff --git a/packages/sdk/helper/src/templates/assets/local-tool.ts.tpl b/packages/scaffold/helper/src/templates/assets/local-tool.ts.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/local-tool.ts.tpl rename to packages/scaffold/helper/src/templates/assets/local-tool.ts.tpl diff --git a/packages/sdk/helper/src/templates/assets/package.json.tpl b/packages/scaffold/helper/src/templates/assets/package.json.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/package.json.tpl rename to packages/scaffold/helper/src/templates/assets/package.json.tpl diff --git a/packages/sdk/helper/src/templates/assets/persona.txt.tpl b/packages/scaffold/helper/src/templates/assets/persona.txt.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/persona.txt.tpl rename to packages/scaffold/helper/src/templates/assets/persona.txt.tpl diff --git a/packages/sdk/helper/src/templates/assets/tsconfig.base.json.tpl b/packages/scaffold/helper/src/templates/assets/tsconfig.base.json.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/tsconfig.base.json.tpl rename to packages/scaffold/helper/src/templates/assets/tsconfig.base.json.tpl diff --git a/packages/sdk/helper/src/templates/assets/tsdown.config.ts.tpl b/packages/scaffold/helper/src/templates/assets/tsdown.config.ts.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/tsdown.config.ts.tpl rename to packages/scaffold/helper/src/templates/assets/tsdown.config.ts.tpl diff --git a/packages/sdk/helper/src/templates/assets/yarnrc.yml.tpl b/packages/scaffold/helper/src/templates/assets/yarnrc.yml.tpl similarity index 100% rename from packages/sdk/helper/src/templates/assets/yarnrc.yml.tpl rename to packages/scaffold/helper/src/templates/assets/yarnrc.yml.tpl diff --git a/packages/sdk/helper/src/templates/project-template.ts b/packages/scaffold/helper/src/templates/project-template.ts similarity index 100% rename from packages/sdk/helper/src/templates/project-template.ts rename to packages/scaffold/helper/src/templates/project-template.ts diff --git a/packages/sdk/helper/src/templates/template-assets.ts b/packages/scaffold/helper/src/templates/template-assets.ts similarity index 100% rename from packages/sdk/helper/src/templates/template-assets.ts rename to packages/scaffold/helper/src/templates/template-assets.ts diff --git a/packages/sdk/helper/src/templates/text-template.ts b/packages/scaffold/helper/src/templates/text-template.ts similarity index 100% rename from packages/sdk/helper/src/templates/text-template.ts rename to packages/scaffold/helper/src/templates/text-template.ts diff --git a/packages/sdk/helper/tests/documents.spec.ts b/packages/scaffold/helper/tests/documents.spec.ts similarity index 100% rename from packages/sdk/helper/tests/documents.spec.ts rename to packages/scaffold/helper/tests/documents.spec.ts diff --git a/packages/sdk/helper/tests/headless-prompt-port.spec.ts b/packages/scaffold/helper/tests/headless-prompt-port.spec.ts similarity index 100% rename from packages/sdk/helper/tests/headless-prompt-port.spec.ts rename to packages/scaffold/helper/tests/headless-prompt-port.spec.ts diff --git a/packages/sdk/helper/tests/project.spec.ts b/packages/scaffold/helper/tests/project.spec.ts similarity index 100% rename from packages/sdk/helper/tests/project.spec.ts rename to packages/scaffold/helper/tests/project.spec.ts diff --git a/packages/sdk/helper/tests/questions.spec.ts b/packages/scaffold/helper/tests/questions.spec.ts similarity index 100% rename from packages/sdk/helper/tests/questions.spec.ts rename to packages/scaffold/helper/tests/questions.spec.ts diff --git a/packages/sdk/helper/tsconfig.json b/packages/scaffold/helper/tsconfig.json similarity index 84% rename from packages/sdk/helper/tsconfig.json rename to packages/scaffold/helper/tsconfig.json index 528b370a5c..f33033f47f 100644 --- a/packages/sdk/helper/tsconfig.json +++ b/packages/scaffold/helper/tsconfig.json @@ -19,10 +19,10 @@ "path": "../../hooks/hooks-codex" }, { - "path": "../../session-persistence/session-persistence-jsonl" + "path": "../../session/session-persistence-jsonl" }, { - "path": "../../session-persistence/session-persistence-sqlite" + "path": "../../session/session-persistence-sqlite" }, { "path": "../../subagent/tool-subagent" diff --git a/packages/sdk/helper/tsdown.config.ts b/packages/scaffold/helper/tsdown.config.ts similarity index 100% rename from packages/sdk/helper/tsdown.config.ts rename to packages/scaffold/helper/tsdown.config.ts diff --git a/packages/scaffold/protocol/README.i18n.yaml b/packages/scaffold/protocol/README.i18n.yaml new file mode 100644 index 0000000000..719a135b37 --- /dev/null +++ b/packages/scaffold/protocol/README.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 packages/scaffold/protocol/README.md +README.md: 88a48957d0d44cec9f776d31eab7d25bd353de5f +README.zh.md: c662d28ec6eea5fe4829030daf210a91fd70ff0f diff --git a/packages/sdk/sdk-protocol/README.md b/packages/scaffold/protocol/README.md similarity index 90% rename from packages/sdk/sdk-protocol/README.md rename to packages/scaffold/protocol/README.md index 2d141e6f62..88a48957d0 100644 --- a/packages/sdk/sdk-protocol/README.md +++ b/packages/scaffold/protocol/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The shared wire protocol for the DeepSeek Harness SDK runtime: one newline-delimited JSON-RPC 2.0 transport class plus the named request, result, and notification types both wire ends speak. The package root enumerates the protocol consumer interface; source modules are not exported as deep imports. The server side is the [`dsh-jsonrpc`](../../ui/jsonrpc/README.md) plugin; clients are [`dsh-sdk-client`](../sdk-client/README.md) (TypeScript) and the [Python SDK](../../../python/README.md) (which mirrors these shapes but does not import them). A pure library — no plugin, no Config, no registration. +The shared wire protocol for the DeepSeek Harness SDK runtime: one newline-delimited JSON-RPC 2.0 transport class plus the named request, result, and notification types both wire ends speak. The package root enumerates the protocol consumer interface; source modules are not exported as deep imports. The server side is the [`dsh-jsonrpc`](../server/README.md) plugin; clients are [`dsh-sdk-client`](../client/README.md) (TypeScript) and the [Python SDK](../../../python/README.md) (which mirrors these shapes but does not import them). A pure library — no plugin, no Config, no registration. ## Transport @@ -26,7 +26,7 @@ The shared wire protocol for the DeepSeek Harness SDK runtime: one newline-delim ## Model Experience -None, as this package defines the client-facing wire protocol; the model-visible surfaces belong to the runtime plugins composed behind the serving [`dsh-jsonrpc`](../../ui/jsonrpc/README.md) entry. +None, as this package defines the client-facing wire protocol; the model-visible surfaces belong to the runtime plugins composed behind the serving [`dsh-jsonrpc`](../server/README.md) entry. #### KV Cache effect @@ -35,5 +35,5 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work - **No protocol-version negotiation** — the handshake carries only `serverInfo.version` (`0.0.1`, unvalidated by clients); pre-release stance, no compatibility promise. -- **No cancel or session-close methods** — a client abandons a turn by closing the runtime process; see the [`dsh-jsonrpc` README](../../ui/jsonrpc/README.md). +- **No cancel or session-close methods** — a client abandons a turn by closing the runtime process; see the [`dsh-jsonrpc` README](../server/README.md). - **Server→client requests are dead capability** — the transport supports them, but the server never sends one; the Python SDK's responder surface exists for future approval flows. diff --git a/packages/sdk/sdk-protocol/README.zh.md b/packages/scaffold/protocol/README.zh.md similarity index 90% rename from packages/sdk/sdk-protocol/README.zh.md rename to packages/scaffold/protocol/README.zh.md index bc459f8817..c662d28ec6 100644 --- a/packages/sdk/sdk-protocol/README.zh.md +++ b/packages/scaffold/protocol/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按换行分帧的 JSON-RPC 2.0 传输类,加上协议两端共同使用的具名请求、结果与通知类型。包根枚举协议消费方接口;源模块不支持深层导入。服务端是 [`dsh-jsonrpc`](../../ui/jsonrpc/README.md) 插件;客户端是 [`dsh-sdk-client`](../sdk-client/README.md)(TypeScript)与 [Python SDK](../../../python/README.md)(后者复现这些结构但不导入它们)。纯库——无插件、无 Config、无注册。 +DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按换行分帧的 JSON-RPC 2.0 传输类,加上协议两端共同使用的具名请求、结果与通知类型。包根枚举协议消费方接口;源模块不支持深层导入。服务端是 [`dsh-jsonrpc`](../server/README.md) 插件;客户端是 [`dsh-sdk-client`](../client/README.md)(TypeScript)与 [Python SDK](../../../python/README.md)(后者复现这些结构但不导入它们)。纯库——无插件、无 Config、无注册。 ## 传输 @@ -26,7 +26,7 @@ DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按 ## 模型体验 -无,因为此包定义面向客户端的协议格式;模型可见接口属于组合在对外服务入口 [`dsh-jsonrpc`](../../ui/jsonrpc/README.md) 后方的运行时插件。 +无,因为此包定义面向客户端的协议格式;模型可见接口属于组合在对外服务入口 [`dsh-jsonrpc`](../server/README.md) 后方的运行时插件。 #### KV Cache 影响 @@ -35,5 +35,5 @@ DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按 ## 已知限制与暂缓事项 - **无协议版本协商**——握手只携带 `serverInfo.version`(`0.0.1`,客户端不校验);处于预发布阶段,无兼容承诺。 -- **无取消与会话关闭方法**——客户端放弃轮次的方式是关闭运行时进程;见 [`dsh-jsonrpc` README](../../ui/jsonrpc/README.md)。 +- **无取消与会话关闭方法**——客户端放弃轮次的方式是关闭运行时进程;见 [`dsh-jsonrpc` README](../server/README.md)。 - **server→client 请求是未使用的功能**——传输层支持,但服务器从不发送;Python SDK 的应答接口为未来审批流程预留。 diff --git a/packages/sdk/sdk-protocol/package.json b/packages/scaffold/protocol/package.json similarity index 100% rename from packages/sdk/sdk-protocol/package.json rename to packages/scaffold/protocol/package.json diff --git a/packages/sdk/sdk-protocol/src/index.ts b/packages/scaffold/protocol/src/index.ts similarity index 100% rename from packages/sdk/sdk-protocol/src/index.ts rename to packages/scaffold/protocol/src/index.ts diff --git a/packages/sdk/sdk-protocol/src/invariant.ts b/packages/scaffold/protocol/src/invariant.ts similarity index 100% rename from packages/sdk/sdk-protocol/src/invariant.ts rename to packages/scaffold/protocol/src/invariant.ts diff --git a/packages/sdk/sdk-protocol/src/transport.ts b/packages/scaffold/protocol/src/transport.ts similarity index 100% rename from packages/sdk/sdk-protocol/src/transport.ts rename to packages/scaffold/protocol/src/transport.ts diff --git a/packages/sdk/sdk-protocol/src/types.ts b/packages/scaffold/protocol/src/types.ts similarity index 100% rename from packages/sdk/sdk-protocol/src/types.ts rename to packages/scaffold/protocol/src/types.ts diff --git a/packages/sdk/sdk-protocol/tests/transport.spec.ts b/packages/scaffold/protocol/tests/transport.spec.ts similarity index 100% rename from packages/sdk/sdk-protocol/tests/transport.spec.ts rename to packages/scaffold/protocol/tests/transport.spec.ts diff --git a/packages/sdk/sdk-protocol/tsconfig.json b/packages/scaffold/protocol/tsconfig.json similarity index 100% rename from packages/sdk/sdk-protocol/tsconfig.json rename to packages/scaffold/protocol/tsconfig.json diff --git a/packages/sdk/scripts/README.i18n.yaml b/packages/scaffold/scripts/README.i18n.yaml similarity index 80% rename from packages/sdk/scripts/README.i18n.yaml rename to packages/scaffold/scripts/README.i18n.yaml index b148eb76ab..db65cb315f 100644 --- a/packages/sdk/scripts/README.i18n.yaml +++ b/packages/scaffold/scripts/README.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 packages/sdk/scripts/README.md +# pnpm run verify-translation-pairing --write packages/scaffold/scripts/README.md README.md: 9a696bf5a4de9a80f0741f07a7e753733bc2f998 README.zh.md: cf9fc2cbbe467eb86e89d9eb77e9bc34860b5a3a diff --git a/packages/sdk/scripts/README.md b/packages/scaffold/scripts/README.md similarity index 100% rename from packages/sdk/scripts/README.md rename to packages/scaffold/scripts/README.md diff --git a/packages/sdk/scripts/README.zh.md b/packages/scaffold/scripts/README.zh.md similarity index 100% rename from packages/sdk/scripts/README.zh.md rename to packages/scaffold/scripts/README.zh.md diff --git a/packages/sdk/scripts/package.json b/packages/scaffold/scripts/package.json similarity index 100% rename from packages/sdk/scripts/package.json rename to packages/scaffold/scripts/package.json diff --git a/packages/sdk/scripts/src/args.ts b/packages/scaffold/scripts/src/args.ts similarity index 100% rename from packages/sdk/scripts/src/args.ts rename to packages/scaffold/scripts/src/args.ts diff --git a/packages/sdk/scripts/src/bin.ts b/packages/scaffold/scripts/src/bin.ts similarity index 100% rename from packages/sdk/scripts/src/bin.ts rename to packages/scaffold/scripts/src/bin.ts diff --git a/packages/sdk/scripts/src/build.ts b/packages/scaffold/scripts/src/build.ts similarity index 100% rename from packages/sdk/scripts/src/build.ts rename to packages/scaffold/scripts/src/build.ts diff --git a/packages/sdk/scripts/src/command.ts b/packages/scaffold/scripts/src/command.ts similarity index 100% rename from packages/sdk/scripts/src/command.ts rename to packages/scaffold/scripts/src/command.ts diff --git a/packages/sdk/scripts/src/config.ts b/packages/scaffold/scripts/src/config.ts similarity index 100% rename from packages/sdk/scripts/src/config.ts rename to packages/scaffold/scripts/src/config.ts diff --git a/packages/sdk/scripts/src/config/config-workflow.ts b/packages/scaffold/scripts/src/config/config-workflow.ts similarity index 100% rename from packages/sdk/scripts/src/config/config-workflow.ts rename to packages/scaffold/scripts/src/config/config-workflow.ts diff --git a/packages/sdk/scripts/src/create-plugin.ts b/packages/scaffold/scripts/src/create-plugin.ts similarity index 100% rename from packages/sdk/scripts/src/create-plugin.ts rename to packages/scaffold/scripts/src/create-plugin.ts diff --git a/packages/sdk/scripts/src/dev/tsdown-config.ts b/packages/scaffold/scripts/src/dev/tsdown-config.ts similarity index 100% rename from packages/sdk/scripts/src/dev/tsdown-config.ts rename to packages/scaffold/scripts/src/dev/tsdown-config.ts diff --git a/packages/scaffold/scripts/src/index.ts b/packages/scaffold/scripts/src/index.ts new file mode 100644 index 0000000000..0db62945f9 --- /dev/null +++ b/packages/scaffold/scripts/src/index.ts @@ -0,0 +1,11 @@ +/** + * Public DeepSeek Harness SDK runtime entry points. + * + * FIXME: rename to `@deepseek-ai/dsh-sdk-scripts` before the first tagged release — + * the current name is indefensibly generic as a published name + * ([regrouping Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md)). + * + * @module @deepseek-ai/dsh-scripts + */ + +export { runSDK, startSDK, type SdkBootContext } from './runtime.ts' diff --git a/packages/sdk/scripts/src/invariant.ts b/packages/scaffold/scripts/src/invariant.ts similarity index 100% rename from packages/sdk/scripts/src/invariant.ts rename to packages/scaffold/scripts/src/invariant.ts diff --git a/packages/sdk/scripts/src/local-plugin-loader-hooks.ts b/packages/scaffold/scripts/src/local-plugin-loader-hooks.ts similarity index 100% rename from packages/sdk/scripts/src/local-plugin-loader-hooks.ts rename to packages/scaffold/scripts/src/local-plugin-loader-hooks.ts diff --git a/packages/sdk/scripts/src/runtime.ts b/packages/scaffold/scripts/src/runtime.ts similarity index 100% rename from packages/sdk/scripts/src/runtime.ts rename to packages/scaffold/scripts/src/runtime.ts diff --git a/packages/sdk/scripts/src/telemetry.ts b/packages/scaffold/scripts/src/telemetry.ts similarity index 100% rename from packages/sdk/scripts/src/telemetry.ts rename to packages/scaffold/scripts/src/telemetry.ts diff --git a/packages/sdk/scripts/src/templates/assets/config-install-failure.txt.tpl b/packages/scaffold/scripts/src/templates/assets/config-install-failure.txt.tpl similarity index 100% rename from packages/sdk/scripts/src/templates/assets/config-install-failure.txt.tpl rename to packages/scaffold/scripts/src/templates/assets/config-install-failure.txt.tpl diff --git a/packages/sdk/scripts/src/templates/assets/usage.txt.tpl b/packages/scaffold/scripts/src/templates/assets/usage.txt.tpl similarity index 100% rename from packages/sdk/scripts/src/templates/assets/usage.txt.tpl rename to packages/scaffold/scripts/src/templates/assets/usage.txt.tpl diff --git a/packages/sdk/scripts/src/templates/dsh-sdk-templates.ts b/packages/scaffold/scripts/src/templates/dsh-sdk-templates.ts similarity index 100% rename from packages/sdk/scripts/src/templates/dsh-sdk-templates.ts rename to packages/scaffold/scripts/src/templates/dsh-sdk-templates.ts diff --git a/packages/sdk/scripts/tests/__snapshots__/config.snapshot.ts.snap b/packages/scaffold/scripts/tests/__snapshots__/config.snapshot.ts.snap similarity index 100% rename from packages/sdk/scripts/tests/__snapshots__/config.snapshot.ts.snap rename to packages/scaffold/scripts/tests/__snapshots__/config.snapshot.ts.snap diff --git a/packages/sdk/scripts/tests/config.snapshot.ts b/packages/scaffold/scripts/tests/config.snapshot.ts similarity index 100% rename from packages/sdk/scripts/tests/config.snapshot.ts rename to packages/scaffold/scripts/tests/config.snapshot.ts diff --git a/packages/sdk/scripts/tests/scripts.spec.ts b/packages/scaffold/scripts/tests/scripts.spec.ts similarity index 100% rename from packages/sdk/scripts/tests/scripts.spec.ts rename to packages/scaffold/scripts/tests/scripts.spec.ts diff --git a/packages/sdk/scripts/tsconfig.json b/packages/scaffold/scripts/tsconfig.json similarity index 88% rename from packages/sdk/scripts/tsconfig.json rename to packages/scaffold/scripts/tsconfig.json index d3dacea10b..2230a377ab 100644 --- a/packages/sdk/scripts/tsconfig.json +++ b/packages/scaffold/scripts/tsconfig.json @@ -8,7 +8,7 @@ "references": [ { "path": "../helper" }, { "path": "../telemetry" }, - { "path": "../../ui/app-boot" }, + { "path": "../../boot/app-boot" }, { "path": "../../../vendor/cordis" }, { "path": "../../support/invariants" } ] diff --git a/packages/sdk/scripts/tsdown.config.ts b/packages/scaffold/scripts/tsdown.config.ts similarity index 100% rename from packages/sdk/scripts/tsdown.config.ts rename to packages/scaffold/scripts/tsdown.config.ts diff --git a/packages/scaffold/server/README.i18n.yaml b/packages/scaffold/server/README.i18n.yaml new file mode 100644 index 0000000000..fc60d047d1 --- /dev/null +++ b/packages/scaffold/server/README.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 packages/scaffold/server/README.md +README.md: 0afb3a38ece57f74729a218dc85348fa6c7fa529 +README.zh.md: 5305434abbd83eb230dcdd81e72120d96da26cda diff --git a/packages/ui/jsonrpc/README.md b/packages/scaffold/server/README.md similarity index 95% rename from packages/ui/jsonrpc/README.md rename to packages/scaffold/server/README.md index 9cd4876b52..0afb3a38ec 100644 --- a/packages/ui/jsonrpc/README.md +++ b/packages/scaffold/server/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-process SDK clients can drive harness agents. [`HarnessSdkServer`](src/server.ts) owns the protocol methods and notifications; the transport and the named wire types live in [`dsh-sdk-protocol`](../../sdk/sdk-protocol/README.md), shared with the client SDKs; [`jsonrpc-demo`](../../examples/jsonrpc-demo/README.md) supplies the surrounding `cordis.yml` application. +The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-process SDK clients can drive harness agents. [`HarnessSdkServer`](src/server.ts) owns the protocol methods and notifications; the transport and the named wire types live in [`dsh-sdk-protocol`](../../scaffold/protocol/README.md), shared with the client SDKs; [`jsonrpc-demo`](../../examples/jsonrpc-demo/README.md) supplies the surrounding `cordis.yml` application. ## Wiring diff --git a/packages/ui/jsonrpc/README.zh.md b/packages/scaffold/server/README.zh.md similarity index 95% rename from packages/ui/jsonrpc/README.zh.md rename to packages/scaffold/server/README.zh.md index a2b979da44..5305434abb 100644 --- a/packages/ui/jsonrpc/README.zh.md +++ b/packages/scaffold/server/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -`jsonrpc` 插件通过 stdio 提供以换行符分隔的 JSON-RPC,使进程外 SDK 客户端能够驱动 harness agent(智能体)。[`HarnessSdkServer`](src/server.ts) 负责协议方法和通知;传输与具名协议类型位于 [`dsh-sdk-protocol`](../../sdk/sdk-protocol/README.md),与客户端 SDK 共享;[`jsonrpc-demo`](../../examples/jsonrpc-demo/README.md) 提供外围的 `cordis.yml` 应用。 +`jsonrpc` 插件通过 stdio 提供以换行符分隔的 JSON-RPC,使进程外 SDK 客户端能够驱动 harness agent(智能体)。[`HarnessSdkServer`](src/server.ts) 负责协议方法和通知;传输与具名协议类型位于 [`dsh-sdk-protocol`](../../scaffold/protocol/README.md),与客户端 SDK 共享;[`jsonrpc-demo`](../../examples/jsonrpc-demo/README.md) 提供外围的 `cordis.yml` 应用。 ## 组装 diff --git a/packages/ui/jsonrpc/package.json b/packages/scaffold/server/package.json similarity index 100% rename from packages/ui/jsonrpc/package.json rename to packages/scaffold/server/package.json diff --git a/packages/ui/jsonrpc/src/index.ts b/packages/scaffold/server/src/index.ts similarity index 92% rename from packages/ui/jsonrpc/src/index.ts rename to packages/scaffold/server/src/index.ts index 423f338877..f0261e1a21 100644 --- a/packages/ui/jsonrpc/src/index.ts +++ b/packages/scaffold/server/src/index.ts @@ -6,6 +6,10 @@ * owns EOF and signal exits. Keep named plugin exports with no default export so * Loader `unwrapExports` preserves `name`, `inject`, `Config`, and `apply`. * + * FIXME: rename to `@deepseek-ai/dsh-sdk-server` before the first tagged release — + * the current name says the wire encoding, not the role; it is the server half of + * the SDK protocol ([regrouping Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md)). + * * @module @deepseek-ai/dsh-jsonrpc */ diff --git a/packages/ui/jsonrpc/src/invariant.ts b/packages/scaffold/server/src/invariant.ts similarity index 100% rename from packages/ui/jsonrpc/src/invariant.ts rename to packages/scaffold/server/src/invariant.ts diff --git a/packages/ui/jsonrpc/src/server.ts b/packages/scaffold/server/src/server.ts similarity index 100% rename from packages/ui/jsonrpc/src/server.ts rename to packages/scaffold/server/src/server.ts diff --git a/packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts b/packages/scaffold/server/tests/built-scope-carrier.e2e.ts similarity index 97% rename from packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts rename to packages/scaffold/server/tests/built-scope-carrier.e2e.ts index 051b0eef20..b3519110b5 100644 --- a/packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts +++ b/packages/scaffold/server/tests/built-scope-carrier.e2e.ts @@ -35,8 +35,8 @@ const [ load("vendor/cordis/lib/index.js"), load("packages/examples/agent-spine-demo/lib/index.js"), load("packages/subagent/subagent/lib/index.js"), - load("packages/session-persistence/session-persistence-jsonl/lib/index.js"), - load("packages/ui/jsonrpc/lib/index.js"), + load("packages/session/session-persistence-jsonl/lib/index.js"), + load("packages/scaffold/server/lib/index.js"), load("packages/core/session/lib/index.js"), ]); diff --git a/packages/ui/jsonrpc/tests/plugin-apply.spec.ts b/packages/scaffold/server/tests/plugin-apply.spec.ts similarity index 100% rename from packages/ui/jsonrpc/tests/plugin-apply.spec.ts rename to packages/scaffold/server/tests/plugin-apply.spec.ts diff --git a/packages/ui/jsonrpc/tests/plugin-shape.spec.ts b/packages/scaffold/server/tests/plugin-shape.spec.ts similarity index 100% rename from packages/ui/jsonrpc/tests/plugin-shape.spec.ts rename to packages/scaffold/server/tests/plugin-shape.spec.ts diff --git a/packages/ui/jsonrpc/tests/server.spec.ts b/packages/scaffold/server/tests/server.spec.ts similarity index 100% rename from packages/ui/jsonrpc/tests/server.spec.ts rename to packages/scaffold/server/tests/server.spec.ts diff --git a/packages/ui/jsonrpc/tsconfig.json b/packages/scaffold/server/tsconfig.json similarity index 93% rename from packages/ui/jsonrpc/tsconfig.json rename to packages/scaffold/server/tsconfig.json index 00ac7000e6..56419b6b84 100644 --- a/packages/ui/jsonrpc/tsconfig.json +++ b/packages/scaffold/server/tsconfig.json @@ -27,7 +27,7 @@ "path": "../../core/session" }, { - "path": "../../sdk/sdk-protocol" + "path": "../protocol" }, { "path": "../../subagent/subagent" diff --git a/packages/sdk/telemetry/README.i18n.yaml b/packages/scaffold/telemetry/README.i18n.yaml similarity index 80% rename from packages/sdk/telemetry/README.i18n.yaml rename to packages/scaffold/telemetry/README.i18n.yaml index 1604b40bf4..4d70fce157 100644 --- a/packages/sdk/telemetry/README.i18n.yaml +++ b/packages/scaffold/telemetry/README.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 packages/sdk/telemetry/README.md +# pnpm run verify-translation-pairing --write packages/scaffold/telemetry/README.md README.md: c87735a93e7659f2913f4dd325176a8ae40cf29b README.zh.md: 24d6e72d988f94cdbc6aa01607edbfc105213267 diff --git a/packages/sdk/telemetry/README.md b/packages/scaffold/telemetry/README.md similarity index 100% rename from packages/sdk/telemetry/README.md rename to packages/scaffold/telemetry/README.md diff --git a/packages/sdk/telemetry/README.zh.md b/packages/scaffold/telemetry/README.zh.md similarity index 100% rename from packages/sdk/telemetry/README.zh.md rename to packages/scaffold/telemetry/README.zh.md diff --git a/packages/sdk/telemetry/package.json b/packages/scaffold/telemetry/package.json similarity index 100% rename from packages/sdk/telemetry/package.json rename to packages/scaffold/telemetry/package.json diff --git a/packages/sdk/telemetry/src/anonymous-id.ts b/packages/scaffold/telemetry/src/anonymous-id.ts similarity index 100% rename from packages/sdk/telemetry/src/anonymous-id.ts rename to packages/scaffold/telemetry/src/anonymous-id.ts diff --git a/packages/sdk/telemetry/src/consent-resolver.ts b/packages/scaffold/telemetry/src/consent-resolver.ts similarity index 100% rename from packages/sdk/telemetry/src/consent-resolver.ts rename to packages/scaffold/telemetry/src/consent-resolver.ts diff --git a/packages/sdk/telemetry/src/index.ts b/packages/scaffold/telemetry/src/index.ts similarity index 82% rename from packages/sdk/telemetry/src/index.ts rename to packages/scaffold/telemetry/src/index.ts index 107956fa39..3c0a5beae7 100644 --- a/packages/sdk/telemetry/src/index.ts +++ b/packages/scaffold/telemetry/src/index.ts @@ -7,6 +7,11 @@ * the launcher command dispatch and the helper feature catalog lives outside * this package. * + * FIXME: rename to `@deepseek-ai/dsh-sdk-telemetry` before the first tagged release — + * the current name collides with the `dsh-session-telemetry` family; this is + * launcher-side SDK telemetry + * ([regrouping Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md)). + * * @module @deepseek-ai/dsh-telemetry */ diff --git a/packages/sdk/telemetry/src/invariant.ts b/packages/scaffold/telemetry/src/invariant.ts similarity index 100% rename from packages/sdk/telemetry/src/invariant.ts rename to packages/scaffold/telemetry/src/invariant.ts diff --git a/packages/sdk/telemetry/src/payload.ts b/packages/scaffold/telemetry/src/payload.ts similarity index 100% rename from packages/sdk/telemetry/src/payload.ts rename to packages/scaffold/telemetry/src/payload.ts diff --git a/packages/sdk/telemetry/src/reporter.ts b/packages/scaffold/telemetry/src/reporter.ts similarity index 100% rename from packages/sdk/telemetry/src/reporter.ts rename to packages/scaffold/telemetry/src/reporter.ts diff --git a/packages/sdk/telemetry/src/secret-redactor.ts b/packages/scaffold/telemetry/src/secret-redactor.ts similarity index 100% rename from packages/sdk/telemetry/src/secret-redactor.ts rename to packages/scaffold/telemetry/src/secret-redactor.ts diff --git a/packages/sdk/telemetry/tests/anonymous-id.spec.ts b/packages/scaffold/telemetry/tests/anonymous-id.spec.ts similarity index 100% rename from packages/sdk/telemetry/tests/anonymous-id.spec.ts rename to packages/scaffold/telemetry/tests/anonymous-id.spec.ts diff --git a/packages/sdk/telemetry/tests/consent-resolver.spec.ts b/packages/scaffold/telemetry/tests/consent-resolver.spec.ts similarity index 100% rename from packages/sdk/telemetry/tests/consent-resolver.spec.ts rename to packages/scaffold/telemetry/tests/consent-resolver.spec.ts diff --git a/packages/sdk/telemetry/tests/payload.spec.ts b/packages/scaffold/telemetry/tests/payload.spec.ts similarity index 100% rename from packages/sdk/telemetry/tests/payload.spec.ts rename to packages/scaffold/telemetry/tests/payload.spec.ts diff --git a/packages/sdk/telemetry/tests/reporter.spec.ts b/packages/scaffold/telemetry/tests/reporter.spec.ts similarity index 100% rename from packages/sdk/telemetry/tests/reporter.spec.ts rename to packages/scaffold/telemetry/tests/reporter.spec.ts diff --git a/packages/sdk/telemetry/tests/secret-redactor.spec.ts b/packages/scaffold/telemetry/tests/secret-redactor.spec.ts similarity index 100% rename from packages/sdk/telemetry/tests/secret-redactor.spec.ts rename to packages/scaffold/telemetry/tests/secret-redactor.spec.ts diff --git a/packages/sdk/telemetry/tsconfig.json b/packages/scaffold/telemetry/tsconfig.json similarity index 100% rename from packages/sdk/telemetry/tsconfig.json rename to packages/scaffold/telemetry/tsconfig.json diff --git a/packages/sdk/README.zh.md b/packages/sdk/README.zh.md deleted file mode 100644 index 003383c5f6..0000000000 --- a/packages/sdk/README.zh.md +++ /dev/null @@ -1,16 +0,0 @@ -# SDK 包 - -[English](README.md) | 中文 - -本分组包含 Harness 项目的开发者工具,以及从另一个进程驱动 Harness 运行时的客户端栈。 - -| 包 | 职责 | -|---|---| -| [`helper/`](helper/README.md) | 提供共享的项目编辑领域 | -| [`scripts/`](scripts/README.md) | 提供 `dsh-sdk` 项目命令 | -| [`create-sdk/`](create-sdk/README.md) | 创建新的 SDK 项目 | -| [`sdk-protocol/`](sdk-protocol/README.md) | 定义 SDK 运行时协议格式 | -| [`sdk-client/`](sdk-client/README.md) | 通过 TypeScript 客户端 API 驱动 Harness 运行时 | -| [`telemetry/`](telemetry/README.md) | 提供启动器遥测、同意和脱敏原语 | - -`@deepseek-ai/create-sdk` 遵循 npm 的 scoped initializer 命名约定;其他包遵循仓库的 `@deepseek-ai/dsh-*` 约定。参见[开发者项目工作流](../../.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md)、[项目编辑架构](../../.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md)和 [TypeScript SDK 设计](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md)。 diff --git a/packages/sdk/scripts/src/index.ts b/packages/sdk/scripts/src/index.ts deleted file mode 100644 index 5180f885c8..0000000000 --- a/packages/sdk/scripts/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Public DeepSeek Harness SDK runtime entry points. - * - * @module @deepseek-ai/dsh-scripts - */ - -export { runSDK, startSDK, type SdkBootContext } from './runtime.ts' diff --git a/packages/sdk/sdk-client/README.i18n.yaml b/packages/sdk/sdk-client/README.i18n.yaml deleted file mode 100644 index bff82b8aec..0000000000 --- a/packages/sdk/sdk-client/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 packages/sdk/sdk-client/README.md -README.md: f27ec256330254156c45b136c21308529ea99e3d -README.zh.md: bd23762500a4da469839da8ddb50455e3419b553 diff --git a/packages/sdk/sdk-protocol/README.i18n.yaml b/packages/sdk/sdk-protocol/README.i18n.yaml deleted file mode 100644 index f878b9fd63..0000000000 --- a/packages/sdk/sdk-protocol/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 packages/sdk/sdk-protocol/README.md -README.md: 2d141e6f62ac0d324933996c282a2d0380af29f2 -README.zh.md: bc459f8817c9477a451015f7629168e246ee531c diff --git a/packages/self-modification/README.i18n.yaml b/packages/self-modification/README.i18n.yaml new file mode 100644 index 0000000000..5a2f02826c --- /dev/null +++ b/packages/self-modification/README.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 packages/self-modification/README.md +README.md: 53f700993cce8df8729fba5a92a9498fff2412cd +README.zh.md: 18e03798b90e73f1d7e61ecf7010b8c472454a08 diff --git a/packages/self-modification/README.md b/packages/self-modification/README.md new file mode 100644 index 0000000000..53f700993c --- /dev/null +++ b/packages/self-modification/README.md @@ -0,0 +1,10 @@ +# self-modification/ — the agent modifies its own runtime + +English | [中文](README.zh.md) + +Model-facing tools over the live cordis runtime the agent itself runs inside: inspect the loaded plugins and service surface, mount model-written plugins, and dispose them again — plus the restricted repository Plugin runtime. The group is the landing zone for future self-modification packages. Design home: [the toolset Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). + +| Package | Role | ctx key | +|---|---|---| +| [`tool-cordis/`](tool-cordis/README.md) | Model-facing runtime inspection and temporary-plugin tools | registers on `ctx.tools` | +| [`repository-plugin/`](repository-plugin/README.md) | Repository skill and MCP composition | registers a Loader builtin | diff --git a/packages/self-modification/README.zh.md b/packages/self-modification/README.zh.md new file mode 100644 index 0000000000..18e03798b9 --- /dev/null +++ b/packages/self-modification/README.zh.md @@ -0,0 +1,10 @@ +# self-modification/:agent 修改自身运行时 + +[English](README.md) | 中文 + +agent 修改自身运行时:检查已加载的插件与服务接口、挂载模型编写的插件并再次 dispose,外加受限 repository Plugin 运行时。该组是未来自我修改类包的落点。设计居所:[工具集 Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)。 + +| 包(package) | 角色 | ctx 键 | +|---|---|---| +| [`tool-cordis/`](tool-cordis/README.md) | `cordis_inspect`/`cordis_mount`/`cordis_unmount` 工具:读取当前进程运行时,并在一个自有分组 fiber 下管理内存中的临时插件 | 注册到 `ctx.tools` | +| [`repository-plugin/`](repository-plugin/README.md) | 通过 DSH 自有子 Plugin 准备并挂载静态 repository skills 与通用 `.mcp.json` server | 注册一个 Loader builtin | diff --git a/packages/cordis/repository-plugin/README.i18n.yaml b/packages/self-modification/repository-plugin/README.i18n.yaml similarity index 77% rename from packages/cordis/repository-plugin/README.i18n.yaml rename to packages/self-modification/repository-plugin/README.i18n.yaml index daf43fa018..e0ce60a641 100644 --- a/packages/cordis/repository-plugin/README.i18n.yaml +++ b/packages/self-modification/repository-plugin/README.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 packages/cordis/repository-plugin/README.md +# pnpm run verify-translation-pairing --write packages/self-modification/repository-plugin/README.md README.md: 33cd763d7dbe21b72f9e604b7b2e313081cf656f README.zh.md: 903dfbe601cc76acb0c1e87453dc03ef0321409b diff --git a/packages/cordis/repository-plugin/README.md b/packages/self-modification/repository-plugin/README.md similarity index 100% rename from packages/cordis/repository-plugin/README.md rename to packages/self-modification/repository-plugin/README.md diff --git a/packages/cordis/repository-plugin/README.zh.md b/packages/self-modification/repository-plugin/README.zh.md similarity index 100% rename from packages/cordis/repository-plugin/README.zh.md rename to packages/self-modification/repository-plugin/README.zh.md diff --git a/packages/cordis/repository-plugin/package.json b/packages/self-modification/repository-plugin/package.json similarity index 100% rename from packages/cordis/repository-plugin/package.json rename to packages/self-modification/repository-plugin/package.json diff --git a/packages/cordis/repository-plugin/src/bin.ts b/packages/self-modification/repository-plugin/src/bin.ts similarity index 100% rename from packages/cordis/repository-plugin/src/bin.ts rename to packages/self-modification/repository-plugin/src/bin.ts diff --git a/packages/cordis/repository-plugin/src/format.ts b/packages/self-modification/repository-plugin/src/format.ts similarity index 100% rename from packages/cordis/repository-plugin/src/format.ts rename to packages/self-modification/repository-plugin/src/format.ts diff --git a/packages/cordis/repository-plugin/src/index.ts b/packages/self-modification/repository-plugin/src/index.ts similarity index 100% rename from packages/cordis/repository-plugin/src/index.ts rename to packages/self-modification/repository-plugin/src/index.ts diff --git a/packages/cordis/repository-plugin/src/invariant.ts b/packages/self-modification/repository-plugin/src/invariant.ts similarity index 100% rename from packages/cordis/repository-plugin/src/invariant.ts rename to packages/self-modification/repository-plugin/src/invariant.ts diff --git a/packages/cordis/repository-plugin/src/mcp.ts b/packages/self-modification/repository-plugin/src/mcp.ts similarity index 100% rename from packages/cordis/repository-plugin/src/mcp.ts rename to packages/self-modification/repository-plugin/src/mcp.ts diff --git a/packages/cordis/repository-plugin/src/source.ts b/packages/self-modification/repository-plugin/src/source.ts similarity index 98% rename from packages/cordis/repository-plugin/src/source.ts rename to packages/self-modification/repository-plugin/src/source.ts index 3ccddc6c52..0befb82e42 100644 --- a/packages/cordis/repository-plugin/src/source.ts +++ b/packages/self-modification/repository-plugin/src/source.ts @@ -11,7 +11,7 @@ import { resolveDshHome } from '@deepseek-ai/dsh-paths' import { PREPARED_ENTRY_FILENAME } from './format.ts' // Value mirror: Cordis's const enum has no runtime object to import. Keep -// aligned with `packages/cordis/tool-cordis/src/fiber-state.ts`. +// aligned with `packages/self-modification/tool-cordis/src/fiber-state.ts`. const FIBER_ACTIVE = 2 as FiberState.ACTIVE /** Directory under the Harness home containing immutable repository generations. */ diff --git a/packages/cordis/repository-plugin/tests/mcp-format.spec.ts b/packages/self-modification/repository-plugin/tests/mcp-format.spec.ts similarity index 100% rename from packages/cordis/repository-plugin/tests/mcp-format.spec.ts rename to packages/self-modification/repository-plugin/tests/mcp-format.spec.ts diff --git a/packages/cordis/repository-plugin/tests/repository-plugin.spec.ts b/packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts similarity index 100% rename from packages/cordis/repository-plugin/tests/repository-plugin.spec.ts rename to packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts diff --git a/packages/cordis/repository-plugin/tsconfig.json b/packages/self-modification/repository-plugin/tsconfig.json similarity index 100% rename from packages/cordis/repository-plugin/tsconfig.json rename to packages/self-modification/repository-plugin/tsconfig.json diff --git a/packages/cordis/repository-plugin/tsdown.config.ts b/packages/self-modification/repository-plugin/tsdown.config.ts similarity index 100% rename from packages/cordis/repository-plugin/tsdown.config.ts rename to packages/self-modification/repository-plugin/tsdown.config.ts diff --git a/packages/self-modification/tool-cordis/README.i18n.yaml b/packages/self-modification/tool-cordis/README.i18n.yaml new file mode 100644 index 0000000000..ef1f441711 --- /dev/null +++ b/packages/self-modification/tool-cordis/README.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 packages/self-modification/tool-cordis/README.md +README.md: f2a65043a1d2f74553e98caf59ed3d38b5a70b7c +README.zh.md: 66742094992d219ccfbd60b935dcd10e48cb12b8 diff --git a/packages/cordis/tool-cordis/README.md b/packages/self-modification/tool-cordis/README.md similarity index 91% rename from packages/cordis/tool-cordis/README.md rename to packages/self-modification/tool-cordis/README.md index 9986310160..f2a65043a1 100644 --- a/packages/cordis/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/cordis/tool-cordis/README.zh.md b/packages/self-modification/tool-cordis/README.zh.md similarity index 90% rename from packages/cordis/tool-cordis/README.zh.md rename to packages/self-modification/tool-cordis/README.zh.md index d955306e1e..6674209499 100644 --- a/packages/cordis/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/cordis/tool-cordis/package.json b/packages/self-modification/tool-cordis/package.json similarity index 100% rename from packages/cordis/tool-cordis/package.json rename to packages/self-modification/tool-cordis/package.json diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/self-modification/tool-cordis/src/api-catalog.ts similarity index 100% rename from packages/cordis/tool-cordis/src/api-catalog.ts rename to packages/self-modification/tool-cordis/src/api-catalog.ts diff --git a/packages/cordis/tool-cordis/src/fiber-state.ts b/packages/self-modification/tool-cordis/src/fiber-state.ts similarity index 100% rename from packages/cordis/tool-cordis/src/fiber-state.ts rename to packages/self-modification/tool-cordis/src/fiber-state.ts diff --git a/packages/cordis/tool-cordis/src/guard.ts b/packages/self-modification/tool-cordis/src/guard.ts similarity index 100% rename from packages/cordis/tool-cordis/src/guard.ts rename to packages/self-modification/tool-cordis/src/guard.ts diff --git a/packages/cordis/tool-cordis/src/index.ts b/packages/self-modification/tool-cordis/src/index.ts similarity index 100% rename from packages/cordis/tool-cordis/src/index.ts rename to packages/self-modification/tool-cordis/src/index.ts diff --git a/packages/cordis/tool-cordis/src/inspect.ts b/packages/self-modification/tool-cordis/src/inspect.ts similarity index 100% rename from packages/cordis/tool-cordis/src/inspect.ts rename to packages/self-modification/tool-cordis/src/inspect.ts diff --git a/packages/cordis/tool-cordis/src/invariant.ts b/packages/self-modification/tool-cordis/src/invariant.ts similarity index 100% rename from packages/cordis/tool-cordis/src/invariant.ts rename to packages/self-modification/tool-cordis/src/invariant.ts diff --git a/packages/cordis/tool-cordis/src/mount.ts b/packages/self-modification/tool-cordis/src/mount.ts similarity index 100% rename from packages/cordis/tool-cordis/src/mount.ts rename to packages/self-modification/tool-cordis/src/mount.ts diff --git a/packages/cordis/tool-cordis/src/present.ts b/packages/self-modification/tool-cordis/src/present.ts similarity index 100% rename from packages/cordis/tool-cordis/src/present.ts rename to packages/self-modification/tool-cordis/src/present.ts diff --git a/packages/cordis/tool-cordis/src/sandbox.ts b/packages/self-modification/tool-cordis/src/sandbox.ts similarity index 100% rename from packages/cordis/tool-cordis/src/sandbox.ts rename to packages/self-modification/tool-cordis/src/sandbox.ts diff --git a/packages/cordis/tool-cordis/tests/cordis-lifecycle.spec.ts b/packages/self-modification/tool-cordis/tests/cordis-lifecycle.spec.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/cordis-lifecycle.spec.ts rename to packages/self-modification/tool-cordis/tests/cordis-lifecycle.spec.ts diff --git a/packages/cordis/tool-cordis/tests/cross-mount.spec.ts b/packages/self-modification/tool-cordis/tests/cross-mount.spec.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/cross-mount.spec.ts rename to packages/self-modification/tool-cordis/tests/cross-mount.spec.ts diff --git a/packages/cordis/tool-cordis/tests/helpers.ts b/packages/self-modification/tool-cordis/tests/helpers.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/helpers.ts rename to packages/self-modification/tool-cordis/tests/helpers.ts diff --git a/packages/cordis/tool-cordis/tests/inspect.spec.ts b/packages/self-modification/tool-cordis/tests/inspect.spec.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/inspect.spec.ts rename to packages/self-modification/tool-cordis/tests/inspect.spec.ts diff --git a/packages/cordis/tool-cordis/tests/integration.spec.ts b/packages/self-modification/tool-cordis/tests/integration.spec.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/integration.spec.ts rename to packages/self-modification/tool-cordis/tests/integration.spec.ts diff --git a/packages/cordis/tool-cordis/tests/mount.spec.ts b/packages/self-modification/tool-cordis/tests/mount.spec.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/mount.spec.ts rename to packages/self-modification/tool-cordis/tests/mount.spec.ts diff --git a/packages/cordis/tool-cordis/tests/present.spec.ts b/packages/self-modification/tool-cordis/tests/present.spec.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/present.spec.ts rename to packages/self-modification/tool-cordis/tests/present.spec.ts diff --git a/packages/cordis/tool-cordis/tests/sandbox-context.spec.ts b/packages/self-modification/tool-cordis/tests/sandbox-context.spec.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/sandbox-context.spec.ts rename to packages/self-modification/tool-cordis/tests/sandbox-context.spec.ts diff --git a/packages/cordis/tool-cordis/tests/tool-cordis.spec.ts b/packages/self-modification/tool-cordis/tests/tool-cordis.spec.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/tool-cordis.spec.ts rename to packages/self-modification/tool-cordis/tests/tool-cordis.spec.ts diff --git a/packages/cordis/tool-cordis/tests/unmount-hmr.spec.ts b/packages/self-modification/tool-cordis/tests/unmount-hmr.spec.ts similarity index 100% rename from packages/cordis/tool-cordis/tests/unmount-hmr.spec.ts rename to packages/self-modification/tool-cordis/tests/unmount-hmr.spec.ts diff --git a/packages/cordis/tool-cordis/tsconfig.json b/packages/self-modification/tool-cordis/tsconfig.json similarity index 100% rename from packages/cordis/tool-cordis/tsconfig.json rename to packages/self-modification/tool-cordis/tsconfig.json diff --git a/packages/session-persistence/README.md b/packages/session-persistence/README.md deleted file mode 100644 index 060f757b35..0000000000 --- a/packages/session-persistence/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# session-persistence/ — persistence capability family - -English | [中文](README.zh.md) - -This family defines durable session persistence, semantic checkpoint policy, and the shipped storage backends. - -| Package | Role | ctx key | -|---|---|---| -| [`session-persistence/`](session-persistence/README.md) | Defines the persistence service and shared write coordination | `ctx.sessionPersistence` | -| [`session-checkpoint-policy/`](session-checkpoint-policy/README.md) | Applies semantic durability checkpoints | wraps `ctx.llm` and `ctx.tools` | -| [`session-persistence-jsonl/`](session-persistence-jsonl/README.md) | Persists sessions in JSONL files | registers on `ctx.sessionPersistence` | -| [`session-persistence-sqlite/`](session-persistence-sqlite/README.md) | Persists sessions in SQLite | registers on `ctx.sessionPersistence` | - -The [session-persistence decision](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md) records the family design. diff --git a/packages/session-persistence/README.zh.md b/packages/session-persistence/README.zh.md deleted file mode 100644 index 67022565f7..0000000000 --- a/packages/session-persistence/README.zh.md +++ /dev/null @@ -1,14 +0,0 @@ -# session-persistence/:持久化能力家族 - -[English](README.md) | 中文 - -本家族定义持久会话数据的持久化机制、语义检查点策略以及随产品交付的存储后端。 - -| 包 | 职责 | ctx 键 | -|---|---|---| -| [`session-persistence/`](session-persistence/README.md) | 定义持久化服务和共享写入协调机制 | `ctx.sessionPersistence` | -| [`session-checkpoint-policy/`](session-checkpoint-policy/README.md) | 应用语义持久性检查点 | 包装 `ctx.llm` 和 `ctx.tools` | -| [`session-persistence-jsonl/`](session-persistence-jsonl/README.md) | 将会话持久化到 JSONL 文件 | 注册到 `ctx.sessionPersistence` | -| [`session-persistence-sqlite/`](session-persistence-sqlite/README.md) | 将会话持久化到 SQLite | 注册到 `ctx.sessionPersistence` | - -[会话持久化决策](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)记录了该家族的设计。 diff --git a/packages/session-projection/README.i18n.yaml b/packages/session-projection/README.i18n.yaml deleted file mode 100644 index 5e03069b99..0000000000 --- a/packages/session-projection/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 packages/session-projection/README.md -README.md: f1fc8337bbea6663915c19f39fdf02f14190370d -README.zh.md: 3323166f496fabafabcd96abeaec707a28ce552a diff --git a/packages/session-projection/README.md b/packages/session-projection/README.md deleted file mode 100644 index f1fc8337bb..0000000000 --- a/packages/session-projection/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# session-projection/ — session projection capability family - -English | [中文](README.zh.md) - -This family serves current, log-derived per-session state to client carriers. - -| Package | Role | ctx key | -|---|---|---| -| [`session-projection/`](session-projection/README.md) | Defines and drives session projection units | `ctx.sessionProjections` | -| [`session-projection-cache/`](session-projection-cache/README.md) | Persists and restores projection checkpoints | `ctx.sessionProjectionCache` | diff --git a/packages/session-projection/README.zh.md b/packages/session-projection/README.zh.md deleted file mode 100644 index 3323166f49..0000000000 --- a/packages/session-projection/README.zh.md +++ /dev/null @@ -1,10 +0,0 @@ -# session-projection/:会话投影能力家族 - -[English](README.md) | 中文 - -本家族向客户端载体提供从日志派生的当前逐会话状态。 - -| 包 | 职责 | ctx 键 | -|---|---|---| -| [`session-projection/`](session-projection/README.md) | 定义并驱动会话投影单元 | `ctx.sessionProjections` | -| [`session-projection-cache/`](session-projection-cache/README.md) | 持久化并恢复投影检查点 | `ctx.sessionProjectionCache` | diff --git a/packages/session-query/README.i18n.yaml b/packages/session-query/README.i18n.yaml index 3911bf76a0..2922f4eb88 100644 --- a/packages/session-query/README.i18n.yaml +++ b/packages/session-query/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/session-query/README.md -README.md: a9c9f33e2dc67e837c2be728d2b1a937ce50f13d -README.zh.md: a29dedba09271baa5836cb2d9d57c753ee41c660 +README.md: 3d5db8da74cb825c701fe50cf518505e9e6bcad9 +README.zh.md: fc430cbdc4130f77eb8857c4d88a2435ad0cc97f diff --git a/packages/session-query/README.md b/packages/session-query/README.md index a9c9f33e2d..3d5db8da74 100644 --- a/packages/session-query/README.md +++ b/packages/session-query/README.md @@ -9,3 +9,5 @@ This family provides authorized retrieval over live and durable session logs, in | [`session-query/`](session-query/README.md) | Defines trusted reads, relationship queries, and search operations | `ctx.sessionQuery` | | [`session-query-sqlite/`](session-query-sqlite/README.md) | Implements session queries with SQLite full-text search | `ctx.sessionQuery` | | [`tool-session-query/`](tool-session-query/README.md) | Exposes workspace-authorized session queries to the model | registers on `ctx.tools` | + +The subsystem reference — logical records, bounded reads, traces, filters, result pages — is [docs/subsystems/session-query.md](../../docs/subsystems/session-query.md). diff --git a/packages/session-query/README.zh.md b/packages/session-query/README.zh.md index a29dedba09..fc430cbdc4 100644 --- a/packages/session-query/README.zh.md +++ b/packages/session-query/README.zh.md @@ -9,3 +9,5 @@ | [`session-query/`](session-query/README.md) | 定义可信读取、关系查询和搜索操作 | `ctx.sessionQuery` | | [`session-query-sqlite/`](session-query-sqlite/README.md) | 使用 SQLite 全文搜索实现会话查询 | `ctx.sessionQuery` | | [`tool-session-query/`](tool-session-query/README.md) | 向模型公开经过工作区授权的会话查询 | 注册到 `ctx.tools` | + +子系统参考——逻辑记录、有界读取、追踪、筛选器、结果页——见 [docs/subsystems/session-query.md](../../docs/subsystems/session-query.md)。 diff --git a/packages/session-query/session-query-sqlite/tsconfig.json b/packages/session-query/session-query-sqlite/tsconfig.json index 1f09565c4a..2a74d8e326 100644 --- a/packages/session-query/session-query-sqlite/tsconfig.json +++ b/packages/session-query/session-query-sqlite/tsconfig.json @@ -21,7 +21,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { "path": "../../support/invariants" diff --git a/packages/session-query/session-query/tsconfig.json b/packages/session-query/session-query/tsconfig.json index 0f17353fee..9e661c1c2c 100644 --- a/packages/session-query/session-query/tsconfig.json +++ b/packages/session-query/session-query/tsconfig.json @@ -24,10 +24,10 @@ "path": "../../core/session" }, { - "path": "../../session-title/session-title" + "path": "../../session/session-title" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { "path": "../../support/invariants" diff --git a/packages/session-title/README.md b/packages/session-title/README.md deleted file mode 100644 index c2ccc1cd6d..0000000000 --- a/packages/session-title/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# session-title/ — log-backed session-title capability family - -English | [中文](README.zh.md) - -This family derives durable session titles from the session log, with an optional model-backed provider. - -| Package | Role | ctx key | -|---|---|---| -| [`session-title/`](session-title/README.md) | Owns title state, fallback behavior, provider registration, and refresh | `ctx.sessionTitle` | -| [`session-title-llm/`](session-title-llm/README.md) | Provides shared model-backed title generation | — | -| [`session-title-first-message-llm/`](session-title-first-message-llm/README.md) | Titles a session from its first eligible human message | registers on `ctx.sessionTitle` | -| [`session-title-all-messages-llm/`](session-title-all-messages-llm/README.md) | Titles a session from all eligible human messages | registers on `ctx.sessionTitle` | - -Deployments may register one model-backed provider; the service retains a deterministic fallback when none is present. diff --git a/packages/session-title/README.zh.md b/packages/session-title/README.zh.md deleted file mode 100644 index b8df266a86..0000000000 --- a/packages/session-title/README.zh.md +++ /dev/null @@ -1,14 +0,0 @@ -# session-title/:日志支持的会话标题能力族 - -[English](README.md) | 中文 - -该包族从会话日志派生持久会话标题,并支持可选的模型后端 provider。 - -| 包 | 职责 | ctx key | -|---|---|---| -| [`session-title/`](session-title/README.md) | 负责标题状态、回退行为、provider 注册与刷新 | `ctx.sessionTitle` | -| [`session-title-llm/`](session-title-llm/README.md) | 提供共享的模型标题生成能力 | — | -| [`session-title-first-message-llm/`](session-title-first-message-llm/README.md) | 根据第一条合格的人类消息生成会话标题 | 注册到 `ctx.sessionTitle` | -| [`session-title-all-messages-llm/`](session-title-all-messages-llm/README.md) | 根据所有合格的人类消息生成会话标题 | 注册到 `ctx.sessionTitle` | - -部署可注册一个模型后端 provider;未注册时,服务仍提供确定性回退。 diff --git a/packages/session-title/session-title/README.i18n.yaml b/packages/session-title/session-title/README.i18n.yaml deleted file mode 100644 index b13c6ce917..0000000000 --- a/packages/session-title/session-title/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 packages/session-title/session-title/README.md -README.md: 9a5ec27c36f3411add37ebe231262eb5d205bc9e -README.zh.md: 3960be40e74507279ed2f21927ee8ad4224fe138 diff --git a/packages/session-title/README.i18n.yaml b/packages/session/README.i18n.yaml similarity index 71% rename from packages/session-title/README.i18n.yaml rename to packages/session/README.i18n.yaml index 2c5d1f2788..3829d946f0 100644 --- a/packages/session-title/README.i18n.yaml +++ b/packages/session/README.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 packages/session-title/README.md -README.md: c2ccc1cd6d329a58f673a9ea5d47ce2f35c9dc41 -README.zh.md: b8df266a861da5cb20a76d0738bba4993218c3d5 +# pnpm run verify-translation-pairing --write packages/session/README.md +README.md: 586d1be0286a0de935b0b08313e6965452b85376 +README.zh.md: 60e58e6d471a48d3ced03a518e96aeeae79110e8 diff --git a/packages/session/README.md b/packages/session/README.md new file mode 100644 index 0000000000..586d1be028 --- /dev/null +++ b/packages/session/README.md @@ -0,0 +1,51 @@ +# session/ — durable session data plane + +English | [中文](README.zh.md) + +The durable family around `core/session`'s live in-memory service: the persistence seam with its storage backends and checkpoint policy, the projection seam that serves whole log-derived values, log-backed titles, and outbound session telemetry. All **product** packages. `session-query/` remains a sibling group: the read/tool surface is consumed independently of persistence internals. + +## Persistence + +Durable session persistence, semantic checkpoint policy, and the shipped storage backends. + +| Package | Role | ctx key | +|---|---|---| +| [`session-persistence/`](session-persistence/README.md) | Defines the persistence service and shared write coordination | `ctx.sessionPersistence` | +| [`session-checkpoint-policy/`](session-checkpoint-policy/README.md) | Applies semantic durability checkpoints | wraps `ctx.llm` and `ctx.tools` | +| [`session-persistence-jsonl/`](session-persistence-jsonl/README.md) | Persists sessions in JSONL files | registers on `ctx.sessionPersistence` | +| [`session-persistence-sqlite/`](session-persistence-sqlite/README.md) | Persists sessions in SQLite | registers on `ctx.sessionPersistence` | + +The [session-persistence decision](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md) records the persistence design. + +## Projection + +Serves current, log-derived per-session state to client carriers. + +| Package | Role | ctx key | +|---|---|---| +| [`session-projection/`](session-projection/README.md) | Defines and drives session projection units | `ctx.sessionProjections` | +| [`session-projection-cache/`](session-projection-cache/README.md) | Persists and restores projection checkpoints | `ctx.sessionProjectionCache` | + +## Titles + +Derives durable session titles from the session log, with an optional model-backed provider. + +| Package | Role | ctx key | +|---|---|---| +| [`session-title/`](session-title/README.md) | Owns title state, fallback behavior, provider registration, and refresh | `ctx.sessionTitle` | +| [`session-title-llm/`](session-title-llm/README.md) | Provides shared model-backed title generation | — | +| [`session-title-first-message-llm/`](session-title-first-message-llm/README.md) | Titles a session from its first eligible human message | registers on `ctx.sessionTitle` | +| [`session-title-all-messages-llm/`](session-title-all-messages-llm/README.md) | Titles a session from all eligible human messages | registers on `ctx.sessionTitle` | + +Deployments may register one model-backed provider; the service retains a deterministic fallback when none is present. + +## Telemetry + +Projects session activity into outbound telemetry and delegates delivery to a configured reporting backend. The [telemetry decision](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md) records the reporting boundary; the [mode decision](../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md) records immediate, feedback-gated, and disabled delivery. + +| Package | Role | +|---|---| +| [`session-telemetry/`](session-telemetry/README.md) | Defines capture, redaction, projection, and live or on-demand backend delivery. | +| [`session-telemetry-otel/`](session-telemetry-otel/README.md) | Delivers telemetry through OpenTelemetry logs in `FULL`, `FEEDBACK_ONLY`, or `DISABLED` mode. | + +The subsystem references: [persistence.md](../../docs/subsystems/persistence.md), [session-projection.md](../../docs/subsystems/session-projection.md), [session-title.md](../../docs/subsystems/session-title.md), and [telemetry.md](../../docs/subsystems/telemetry.md). Only one title provider may register at a time; the demo spine mounts the fallback service and leaves both model providers out of default composition. diff --git a/packages/session/README.zh.md b/packages/session/README.zh.md new file mode 100644 index 0000000000..60e58e6d47 --- /dev/null +++ b/packages/session/README.zh.md @@ -0,0 +1,51 @@ +# session/:持久会话数据平面 + +[English](README.md) | 中文 + +围绕 `core/session` 常驻内存实时服务的持久家族:持久化 seam 连同其存储后端与检查点策略、供出日志派生全量值的投影 seam、日志支持的标题,以及外发会话遥测。全部都是**产品**包(package)。`session-query/` 仍是同级独立组:读取/工具面的消费不依赖持久化内部实现。 + +## 持久化 + +持久会话数据的持久化机制、语义检查点策略以及随产品交付的存储后端。 + +| 包 | 职责 | ctx 键 | +|---|---|---| +| [`session-persistence/`](session-persistence/README.md) | 定义持久化服务和共享写入协调机制 | `ctx.sessionPersistence` | +| [`session-checkpoint-policy/`](session-checkpoint-policy/README.md) | 应用语义持久性检查点 | 包装 `ctx.llm` 和 `ctx.tools` | +| [`session-persistence-jsonl/`](session-persistence-jsonl/README.md) | 将会话持久化到 JSONL 文件 | 注册到 `ctx.sessionPersistence` | +| [`session-persistence-sqlite/`](session-persistence-sqlite/README.md) | 将会话持久化到 SQLite | 注册到 `ctx.sessionPersistence` | + +[会话持久化决策](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)记录了持久化设计。 + +## 投影 + +向客户端载体提供从日志派生的当前逐会话状态。 + +| 包 | 职责 | ctx 键 | +|---|---|---| +| [`session-projection/`](session-projection/README.md) | 定义并驱动会话投影单元 | `ctx.sessionProjections` | +| [`session-projection-cache/`](session-projection-cache/README.md) | 持久化并恢复投影检查点 | `ctx.sessionProjectionCache` | + +## 标题 + +从会话日志派生持久会话标题,并支持可选的模型后端 provider。 + +| 包 | 职责 | ctx 键 | +|---|---|---| +| [`session-title/`](session-title/README.md) | 负责标题状态、回退行为、provider 注册与刷新 | `ctx.sessionTitle` | +| [`session-title-llm/`](session-title-llm/README.md) | 提供共享的模型标题生成能力 | — | +| [`session-title-first-message-llm/`](session-title-first-message-llm/README.md) | 根据第一条合格的人类消息生成会话标题 | 注册到 `ctx.sessionTitle` | +| [`session-title-all-messages-llm/`](session-title-all-messages-llm/README.md) | 根据所有合格的人类消息生成会话标题 | 注册到 `ctx.sessionTitle` | + +部署可注册一个模型后端 provider;未注册时,服务仍提供确定性回退。 + +## 遥测 + +将会话活动投影为外发遥测,并将投递委派给配置的上报后端。[遥测决策](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)记录上报边界;[模式决策](../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md)记录即时、反馈门控与禁用投递。 + +| 包 | 职责 | +|---|---| +| [`session-telemetry/`](session-telemetry/README.md) | 定义捕获、脱敏、投影,以及实时或按需后端投递。 | +| [`session-telemetry-otel/`](session-telemetry-otel/README.md) | 通过 OpenTelemetry 日志以 `FULL`、`FEEDBACK_ONLY` 或 `DISABLED` 模式投递遥测。 | + +子系统参考:[persistence.md](../../docs/subsystems/persistence.md)、[session-projection.md](../../docs/subsystems/session-projection.md)、[session-title.md](../../docs/subsystems/session-title.md) 与 [telemetry.md](../../docs/subsystems/telemetry.md)。同一时间只允许一个标题提供方注册;demo 主干挂载回退服务,两个模型提供方都留在默认组合之外。 diff --git a/packages/session-persistence/session-checkpoint-policy/README.i18n.yaml b/packages/session/session-checkpoint-policy/README.i18n.yaml similarity index 100% rename from packages/session-persistence/session-checkpoint-policy/README.i18n.yaml rename to packages/session/session-checkpoint-policy/README.i18n.yaml diff --git a/packages/session-persistence/session-checkpoint-policy/README.md b/packages/session/session-checkpoint-policy/README.md similarity index 100% rename from packages/session-persistence/session-checkpoint-policy/README.md rename to packages/session/session-checkpoint-policy/README.md diff --git a/packages/session-persistence/session-checkpoint-policy/README.zh.md b/packages/session/session-checkpoint-policy/README.zh.md similarity index 100% rename from packages/session-persistence/session-checkpoint-policy/README.zh.md rename to packages/session/session-checkpoint-policy/README.zh.md diff --git a/packages/session-persistence/session-checkpoint-policy/package.json b/packages/session/session-checkpoint-policy/package.json similarity index 100% rename from packages/session-persistence/session-checkpoint-policy/package.json rename to packages/session/session-checkpoint-policy/package.json diff --git a/packages/session-persistence/session-checkpoint-policy/src/index.ts b/packages/session/session-checkpoint-policy/src/index.ts similarity index 100% rename from packages/session-persistence/session-checkpoint-policy/src/index.ts rename to packages/session/session-checkpoint-policy/src/index.ts diff --git a/packages/session-persistence/session-checkpoint-policy/src/invariant.ts b/packages/session/session-checkpoint-policy/src/invariant.ts similarity index 100% rename from packages/session-persistence/session-checkpoint-policy/src/invariant.ts rename to packages/session/session-checkpoint-policy/src/invariant.ts diff --git a/packages/session-persistence/session-checkpoint-policy/tests/crash-recovery.e2e.ts b/packages/session/session-checkpoint-policy/tests/crash-recovery.e2e.ts similarity index 100% rename from packages/session-persistence/session-checkpoint-policy/tests/crash-recovery.e2e.ts rename to packages/session/session-checkpoint-policy/tests/crash-recovery.e2e.ts diff --git a/packages/session-persistence/session-checkpoint-policy/tests/fixtures/crash-child.ts b/packages/session/session-checkpoint-policy/tests/fixtures/crash-child.ts similarity index 100% rename from packages/session-persistence/session-checkpoint-policy/tests/fixtures/crash-child.ts rename to packages/session/session-checkpoint-policy/tests/fixtures/crash-child.ts diff --git a/packages/session-persistence/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts b/packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts similarity index 100% rename from packages/session-persistence/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts rename to packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts diff --git a/packages/session-persistence/session-checkpoint-policy/tsconfig.json b/packages/session/session-checkpoint-policy/tsconfig.json similarity index 89% rename from packages/session-persistence/session-checkpoint-policy/tsconfig.json rename to packages/session/session-checkpoint-policy/tsconfig.json index 1b81e05951..400f67a53b 100644 --- a/packages/session-persistence/session-checkpoint-policy/tsconfig.json +++ b/packages/session/session-checkpoint-policy/tsconfig.json @@ -24,7 +24,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../session-persistence" }, { "path": "../../support/invariants" diff --git a/packages/session-persistence/session-persistence-jsonl/README.i18n.yaml b/packages/session/session-persistence-jsonl/README.i18n.yaml similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/README.i18n.yaml rename to packages/session/session-persistence-jsonl/README.i18n.yaml diff --git a/packages/session-persistence/session-persistence-jsonl/README.md b/packages/session/session-persistence-jsonl/README.md similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/README.md rename to packages/session/session-persistence-jsonl/README.md diff --git a/packages/session-persistence/session-persistence-jsonl/README.zh.md b/packages/session/session-persistence-jsonl/README.zh.md similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/README.zh.md rename to packages/session/session-persistence-jsonl/README.zh.md diff --git a/packages/session-persistence/session-persistence-jsonl/package.json b/packages/session/session-persistence-jsonl/package.json similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/package.json rename to packages/session/session-persistence-jsonl/package.json diff --git a/packages/session-persistence/session-persistence-jsonl/src/format.ts b/packages/session/session-persistence-jsonl/src/format.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/src/format.ts rename to packages/session/session-persistence-jsonl/src/format.ts diff --git a/packages/session-persistence/session-persistence-jsonl/src/index.ts b/packages/session/session-persistence-jsonl/src/index.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/src/index.ts rename to packages/session/session-persistence-jsonl/src/index.ts diff --git a/packages/session-persistence/session-persistence-jsonl/src/invariant.ts b/packages/session/session-persistence-jsonl/src/invariant.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/src/invariant.ts rename to packages/session/session-persistence-jsonl/src/invariant.ts diff --git a/packages/session-persistence/session-persistence-jsonl/src/win32.ts b/packages/session/session-persistence-jsonl/src/win32.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/src/win32.ts rename to packages/session/session-persistence-jsonl/src/win32.ts diff --git a/packages/session-persistence/session-persistence-jsonl/src/zstd-private-decoder.ts b/packages/session/session-persistence-jsonl/src/zstd-private-decoder.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/src/zstd-private-decoder.ts rename to packages/session/session-persistence-jsonl/src/zstd-private-decoder.ts diff --git a/packages/session-persistence/session-persistence-jsonl/src/zstd-public-decoder.ts b/packages/session/session-persistence-jsonl/src/zstd-public-decoder.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/src/zstd-public-decoder.ts rename to packages/session/session-persistence-jsonl/src/zstd-public-decoder.ts diff --git a/packages/session-persistence/session-persistence-jsonl/src/zstd.ts b/packages/session/session-persistence-jsonl/src/zstd.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/src/zstd.ts rename to packages/session/session-persistence-jsonl/src/zstd.ts diff --git a/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts b/packages/session/session-persistence-jsonl/tests/jsonl.spec.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts rename to packages/session/session-persistence-jsonl/tests/jsonl.spec.ts diff --git a/packages/session-persistence/session-persistence-jsonl/tests/win32.spec.ts b/packages/session/session-persistence-jsonl/tests/win32.spec.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/tests/win32.spec.ts rename to packages/session/session-persistence-jsonl/tests/win32.spec.ts diff --git a/packages/session-persistence/session-persistence-jsonl/tests/zstd.compat.spec.ts b/packages/session/session-persistence-jsonl/tests/zstd.compat.spec.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/tests/zstd.compat.spec.ts rename to packages/session/session-persistence-jsonl/tests/zstd.compat.spec.ts diff --git a/packages/session-persistence/session-persistence-jsonl/tests/zstd.spec.ts b/packages/session/session-persistence-jsonl/tests/zstd.spec.ts similarity index 100% rename from packages/session-persistence/session-persistence-jsonl/tests/zstd.spec.ts rename to packages/session/session-persistence-jsonl/tests/zstd.spec.ts diff --git a/packages/session-persistence/session-persistence-sqlite/tsconfig.json b/packages/session/session-persistence-jsonl/tsconfig.json similarity index 87% rename from packages/session-persistence/session-persistence-sqlite/tsconfig.json rename to packages/session/session-persistence-jsonl/tsconfig.json index 044156938b..240c0b0ea9 100644 --- a/packages/session-persistence/session-persistence-sqlite/tsconfig.json +++ b/packages/session/session-persistence-jsonl/tsconfig.json @@ -21,7 +21,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../session-persistence" }, { "path": "../../support/invariants" diff --git a/packages/session-persistence/session-persistence-sqlite/README.i18n.yaml b/packages/session/session-persistence-sqlite/README.i18n.yaml similarity index 100% rename from packages/session-persistence/session-persistence-sqlite/README.i18n.yaml rename to packages/session/session-persistence-sqlite/README.i18n.yaml diff --git a/packages/session-persistence/session-persistence-sqlite/README.md b/packages/session/session-persistence-sqlite/README.md similarity index 100% rename from packages/session-persistence/session-persistence-sqlite/README.md rename to packages/session/session-persistence-sqlite/README.md diff --git a/packages/session-persistence/session-persistence-sqlite/README.zh.md b/packages/session/session-persistence-sqlite/README.zh.md similarity index 100% rename from packages/session-persistence/session-persistence-sqlite/README.zh.md rename to packages/session/session-persistence-sqlite/README.zh.md diff --git a/packages/session-persistence/session-persistence-sqlite/package.json b/packages/session/session-persistence-sqlite/package.json similarity index 100% rename from packages/session-persistence/session-persistence-sqlite/package.json rename to packages/session/session-persistence-sqlite/package.json diff --git a/packages/session-persistence/session-persistence-sqlite/src/index.ts b/packages/session/session-persistence-sqlite/src/index.ts similarity index 100% rename from packages/session-persistence/session-persistence-sqlite/src/index.ts rename to packages/session/session-persistence-sqlite/src/index.ts diff --git a/packages/session-persistence/session-persistence-sqlite/src/invariant.ts b/packages/session/session-persistence-sqlite/src/invariant.ts similarity index 100% rename from packages/session-persistence/session-persistence-sqlite/src/invariant.ts rename to packages/session/session-persistence-sqlite/src/invariant.ts diff --git a/packages/session-persistence/session-persistence-sqlite/src/schema.ts b/packages/session/session-persistence-sqlite/src/schema.ts similarity index 100% rename from packages/session-persistence/session-persistence-sqlite/src/schema.ts rename to packages/session/session-persistence-sqlite/src/schema.ts diff --git a/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts b/packages/session/session-persistence-sqlite/tests/sqlite.spec.ts similarity index 100% rename from packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts rename to packages/session/session-persistence-sqlite/tests/sqlite.spec.ts diff --git a/packages/session-persistence/session-persistence-jsonl/tsconfig.json b/packages/session/session-persistence-sqlite/tsconfig.json similarity index 87% rename from packages/session-persistence/session-persistence-jsonl/tsconfig.json rename to packages/session/session-persistence-sqlite/tsconfig.json index 044156938b..240c0b0ea9 100644 --- a/packages/session-persistence/session-persistence-jsonl/tsconfig.json +++ b/packages/session/session-persistence-sqlite/tsconfig.json @@ -21,7 +21,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../session-persistence" }, { "path": "../../support/invariants" diff --git a/packages/session-persistence/session-persistence/README.i18n.yaml b/packages/session/session-persistence/README.i18n.yaml similarity index 100% rename from packages/session-persistence/session-persistence/README.i18n.yaml rename to packages/session/session-persistence/README.i18n.yaml diff --git a/packages/session-persistence/session-persistence/README.md b/packages/session/session-persistence/README.md similarity index 100% rename from packages/session-persistence/session-persistence/README.md rename to packages/session/session-persistence/README.md diff --git a/packages/session-persistence/session-persistence/README.zh.md b/packages/session/session-persistence/README.zh.md similarity index 100% rename from packages/session-persistence/session-persistence/README.zh.md rename to packages/session/session-persistence/README.zh.md diff --git a/packages/session-persistence/session-persistence/package.json b/packages/session/session-persistence/package.json similarity index 100% rename from packages/session-persistence/session-persistence/package.json rename to packages/session/session-persistence/package.json diff --git a/packages/session-persistence/session-persistence/src/coordinator.ts b/packages/session/session-persistence/src/coordinator.ts similarity index 100% rename from packages/session-persistence/session-persistence/src/coordinator.ts rename to packages/session/session-persistence/src/coordinator.ts diff --git a/packages/session-persistence/session-persistence/src/index.ts b/packages/session/session-persistence/src/index.ts similarity index 100% rename from packages/session-persistence/session-persistence/src/index.ts rename to packages/session/session-persistence/src/index.ts diff --git a/packages/session-persistence/session-persistence/src/invariant.ts b/packages/session/session-persistence/src/invariant.ts similarity index 100% rename from packages/session-persistence/session-persistence/src/invariant.ts rename to packages/session/session-persistence/src/invariant.ts diff --git a/packages/session-persistence/session-persistence/src/preparations.ts b/packages/session/session-persistence/src/preparations.ts similarity index 100% rename from packages/session-persistence/session-persistence/src/preparations.ts rename to packages/session/session-persistence/src/preparations.ts diff --git a/packages/session-persistence/session-persistence/src/revision.ts b/packages/session/session-persistence/src/revision.ts similarity index 100% rename from packages/session-persistence/session-persistence/src/revision.ts rename to packages/session/session-persistence/src/revision.ts diff --git a/packages/session-persistence/session-persistence/src/write-behind.ts b/packages/session/session-persistence/src/write-behind.ts similarity index 100% rename from packages/session-persistence/session-persistence/src/write-behind.ts rename to packages/session/session-persistence/src/write-behind.ts diff --git a/packages/session-persistence/session-persistence/tests/contract.ts b/packages/session/session-persistence/tests/contract.ts similarity index 100% rename from packages/session-persistence/session-persistence/tests/contract.ts rename to packages/session/session-persistence/tests/contract.ts diff --git a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts b/packages/session/session-persistence/tests/coordinator-contract.ts similarity index 100% rename from packages/session-persistence/session-persistence/tests/coordinator-contract.ts rename to packages/session/session-persistence/tests/coordinator-contract.ts diff --git a/packages/session-persistence/session-persistence/tests/persistence.spec.ts b/packages/session/session-persistence/tests/persistence.spec.ts similarity index 100% rename from packages/session-persistence/session-persistence/tests/persistence.spec.ts rename to packages/session/session-persistence/tests/persistence.spec.ts diff --git a/packages/session-persistence/session-persistence/tests/preparations.spec.ts b/packages/session/session-persistence/tests/preparations.spec.ts similarity index 100% rename from packages/session-persistence/session-persistence/tests/preparations.spec.ts rename to packages/session/session-persistence/tests/preparations.spec.ts diff --git a/packages/session-persistence/session-persistence/tests/write-behind.spec.ts b/packages/session/session-persistence/tests/write-behind.spec.ts similarity index 100% rename from packages/session-persistence/session-persistence/tests/write-behind.spec.ts rename to packages/session/session-persistence/tests/write-behind.spec.ts diff --git a/packages/session-persistence/session-persistence/tsconfig.json b/packages/session/session-persistence/tsconfig.json similarity index 100% rename from packages/session-persistence/session-persistence/tsconfig.json rename to packages/session/session-persistence/tsconfig.json diff --git a/packages/session-projection/session-projection-cache/README.i18n.yaml b/packages/session/session-projection-cache/README.i18n.yaml similarity index 100% rename from packages/session-projection/session-projection-cache/README.i18n.yaml rename to packages/session/session-projection-cache/README.i18n.yaml diff --git a/packages/session-projection/session-projection-cache/README.md b/packages/session/session-projection-cache/README.md similarity index 100% rename from packages/session-projection/session-projection-cache/README.md rename to packages/session/session-projection-cache/README.md diff --git a/packages/session-projection/session-projection-cache/README.zh.md b/packages/session/session-projection-cache/README.zh.md similarity index 100% rename from packages/session-projection/session-projection-cache/README.zh.md rename to packages/session/session-projection-cache/README.zh.md diff --git a/packages/session-projection/session-projection-cache/package.json b/packages/session/session-projection-cache/package.json similarity index 100% rename from packages/session-projection/session-projection-cache/package.json rename to packages/session/session-projection-cache/package.json diff --git a/packages/session-projection/session-projection-cache/src/index.ts b/packages/session/session-projection-cache/src/index.ts similarity index 100% rename from packages/session-projection/session-projection-cache/src/index.ts rename to packages/session/session-projection-cache/src/index.ts diff --git a/packages/session-projection/session-projection-cache/src/invariant.ts b/packages/session/session-projection-cache/src/invariant.ts similarity index 100% rename from packages/session-projection/session-projection-cache/src/invariant.ts rename to packages/session/session-projection-cache/src/invariant.ts diff --git a/packages/session-projection/session-projection-cache/src/spec.ts b/packages/session/session-projection-cache/src/spec.ts similarity index 100% rename from packages/session-projection/session-projection-cache/src/spec.ts rename to packages/session/session-projection-cache/src/spec.ts diff --git a/packages/session-projection/session-projection-cache/tests/cache.spec.ts b/packages/session/session-projection-cache/tests/cache.spec.ts similarity index 100% rename from packages/session-projection/session-projection-cache/tests/cache.spec.ts rename to packages/session/session-projection-cache/tests/cache.spec.ts diff --git a/packages/session-projection/session-projection-cache/tsconfig.json b/packages/session/session-projection-cache/tsconfig.json similarity index 90% rename from packages/session-projection/session-projection-cache/tsconfig.json rename to packages/session/session-projection-cache/tsconfig.json index baba6c805b..cf253dbe79 100644 --- a/packages/session-projection/session-projection-cache/tsconfig.json +++ b/packages/session/session-projection-cache/tsconfig.json @@ -21,7 +21,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../session-persistence" }, { "path": "../session-projection" diff --git a/packages/session-projection/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml similarity index 100% rename from packages/session-projection/session-projection/README.i18n.yaml rename to packages/session/session-projection/README.i18n.yaml diff --git a/packages/session-projection/session-projection/README.md b/packages/session/session-projection/README.md similarity index 100% rename from packages/session-projection/session-projection/README.md rename to packages/session/session-projection/README.md diff --git a/packages/session-projection/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md similarity index 100% rename from packages/session-projection/session-projection/README.zh.md rename to packages/session/session-projection/README.zh.md diff --git a/packages/session-projection/session-projection/package.json b/packages/session/session-projection/package.json similarity index 100% rename from packages/session-projection/session-projection/package.json rename to packages/session/session-projection/package.json diff --git a/packages/session-projection/session-projection/src/index.ts b/packages/session/session-projection/src/index.ts similarity index 100% rename from packages/session-projection/session-projection/src/index.ts rename to packages/session/session-projection/src/index.ts diff --git a/packages/session-projection/session-projection/src/invariant.ts b/packages/session/session-projection/src/invariant.ts similarity index 100% rename from packages/session-projection/session-projection/src/invariant.ts rename to packages/session/session-projection/src/invariant.ts diff --git a/packages/session-projection/session-projection/src/types.ts b/packages/session/session-projection/src/types.ts similarity index 100% rename from packages/session-projection/session-projection/src/types.ts rename to packages/session/session-projection/src/types.ts diff --git a/packages/session-projection/session-projection/tests/registry.spec.ts b/packages/session/session-projection/tests/registry.spec.ts similarity index 100% rename from packages/session-projection/session-projection/tests/registry.spec.ts rename to packages/session/session-projection/tests/registry.spec.ts diff --git a/packages/session-projection/session-projection/tsconfig.json b/packages/session/session-projection/tsconfig.json similarity index 100% rename from packages/session-projection/session-projection/tsconfig.json rename to packages/session/session-projection/tsconfig.json diff --git a/packages/telemetry/session-telemetry-otel/README.i18n.yaml b/packages/session/session-telemetry-otel/README.i18n.yaml similarity index 100% rename from packages/telemetry/session-telemetry-otel/README.i18n.yaml rename to packages/session/session-telemetry-otel/README.i18n.yaml diff --git a/packages/telemetry/session-telemetry-otel/README.md b/packages/session/session-telemetry-otel/README.md similarity index 100% rename from packages/telemetry/session-telemetry-otel/README.md rename to packages/session/session-telemetry-otel/README.md diff --git a/packages/telemetry/session-telemetry-otel/README.zh.md b/packages/session/session-telemetry-otel/README.zh.md similarity index 100% rename from packages/telemetry/session-telemetry-otel/README.zh.md rename to packages/session/session-telemetry-otel/README.zh.md diff --git a/packages/telemetry/session-telemetry-otel/package.json b/packages/session/session-telemetry-otel/package.json similarity index 100% rename from packages/telemetry/session-telemetry-otel/package.json rename to packages/session/session-telemetry-otel/package.json diff --git a/packages/telemetry/session-telemetry-otel/src/index.ts b/packages/session/session-telemetry-otel/src/index.ts similarity index 100% rename from packages/telemetry/session-telemetry-otel/src/index.ts rename to packages/session/session-telemetry-otel/src/index.ts diff --git a/packages/telemetry/session-telemetry-otel/src/invariant.ts b/packages/session/session-telemetry-otel/src/invariant.ts similarity index 100% rename from packages/telemetry/session-telemetry-otel/src/invariant.ts rename to packages/session/session-telemetry-otel/src/invariant.ts diff --git a/packages/telemetry/session-telemetry-otel/src/user-id.ts b/packages/session/session-telemetry-otel/src/user-id.ts similarity index 100% rename from packages/telemetry/session-telemetry-otel/src/user-id.ts rename to packages/session/session-telemetry-otel/src/user-id.ts diff --git a/packages/telemetry/session-telemetry-otel/tests/loader-composition.e2e.ts b/packages/session/session-telemetry-otel/tests/loader-composition.e2e.ts similarity index 100% rename from packages/telemetry/session-telemetry-otel/tests/loader-composition.e2e.ts rename to packages/session/session-telemetry-otel/tests/loader-composition.e2e.ts diff --git a/packages/telemetry/session-telemetry-otel/tests/otel.spec.ts b/packages/session/session-telemetry-otel/tests/otel.spec.ts similarity index 100% rename from packages/telemetry/session-telemetry-otel/tests/otel.spec.ts rename to packages/session/session-telemetry-otel/tests/otel.spec.ts diff --git a/packages/telemetry/session-telemetry-otel/tests/user-id.spec.ts b/packages/session/session-telemetry-otel/tests/user-id.spec.ts similarity index 100% rename from packages/telemetry/session-telemetry-otel/tests/user-id.spec.ts rename to packages/session/session-telemetry-otel/tests/user-id.spec.ts diff --git a/packages/telemetry/session-telemetry-otel/tsconfig.json b/packages/session/session-telemetry-otel/tsconfig.json similarity index 100% rename from packages/telemetry/session-telemetry-otel/tsconfig.json rename to packages/session/session-telemetry-otel/tsconfig.json diff --git a/packages/telemetry/session-telemetry/README.i18n.yaml b/packages/session/session-telemetry/README.i18n.yaml similarity index 100% rename from packages/telemetry/session-telemetry/README.i18n.yaml rename to packages/session/session-telemetry/README.i18n.yaml diff --git a/packages/telemetry/session-telemetry/README.md b/packages/session/session-telemetry/README.md similarity index 100% rename from packages/telemetry/session-telemetry/README.md rename to packages/session/session-telemetry/README.md diff --git a/packages/telemetry/session-telemetry/README.zh.md b/packages/session/session-telemetry/README.zh.md similarity index 100% rename from packages/telemetry/session-telemetry/README.zh.md rename to packages/session/session-telemetry/README.zh.md diff --git a/packages/telemetry/session-telemetry/package.json b/packages/session/session-telemetry/package.json similarity index 100% rename from packages/telemetry/session-telemetry/package.json rename to packages/session/session-telemetry/package.json diff --git a/packages/telemetry/session-telemetry/src/coordinator.ts b/packages/session/session-telemetry/src/coordinator.ts similarity index 100% rename from packages/telemetry/session-telemetry/src/coordinator.ts rename to packages/session/session-telemetry/src/coordinator.ts diff --git a/packages/telemetry/session-telemetry/src/index.ts b/packages/session/session-telemetry/src/index.ts similarity index 96% rename from packages/telemetry/session-telemetry/src/index.ts rename to packages/session/session-telemetry/src/index.ts index 0198df3140..6a8e9ed2ad 100644 --- a/packages/telemetry/session-telemetry/src/index.ts +++ b/packages/session/session-telemetry/src/index.ts @@ -71,10 +71,10 @@ export interface TelemetryRecord { /** * Identity attributes, deliberately minimal: ledger records carry * `session.id`, `event.type`, `event.seq`, plus `session.cwd` / - * `session.parent_id` when the header has them; ops records carry - * `telemetry.op`, `session.id`, and (for `agent-error`) `agent.id`, - * `turn`, `step`, `error.name`. Anything recoverable from the body is - * intentionally NOT duplicated here. + * `session.parent_id` / `session.seed_length` when the header has them; + * ops records carry `telemetry.op`, `session.id`, and (for `agent-error`) + * `agent.id`, `turn`, `step`, `error.name`. Anything recoverable from the + * body is intentionally NOT duplicated here. */ attributes: Record /** diff --git a/packages/telemetry/session-telemetry/src/invariant.ts b/packages/session/session-telemetry/src/invariant.ts similarity index 100% rename from packages/telemetry/session-telemetry/src/invariant.ts rename to packages/session/session-telemetry/src/invariant.ts diff --git a/packages/telemetry/session-telemetry/tests/redact.spec.ts b/packages/session/session-telemetry/tests/redact.spec.ts similarity index 100% rename from packages/telemetry/session-telemetry/tests/redact.spec.ts rename to packages/session/session-telemetry/tests/redact.spec.ts diff --git a/packages/telemetry/session-telemetry/tests/telemetry.spec.ts b/packages/session/session-telemetry/tests/telemetry.spec.ts similarity index 100% rename from packages/telemetry/session-telemetry/tests/telemetry.spec.ts rename to packages/session/session-telemetry/tests/telemetry.spec.ts diff --git a/packages/telemetry/session-telemetry/tsconfig.json b/packages/session/session-telemetry/tsconfig.json similarity index 100% rename from packages/telemetry/session-telemetry/tsconfig.json rename to packages/session/session-telemetry/tsconfig.json diff --git a/packages/session-title/session-title-all-messages-llm/README.i18n.yaml b/packages/session/session-title-all-messages-llm/README.i18n.yaml similarity index 100% rename from packages/session-title/session-title-all-messages-llm/README.i18n.yaml rename to packages/session/session-title-all-messages-llm/README.i18n.yaml diff --git a/packages/session-title/session-title-all-messages-llm/README.md b/packages/session/session-title-all-messages-llm/README.md similarity index 100% rename from packages/session-title/session-title-all-messages-llm/README.md rename to packages/session/session-title-all-messages-llm/README.md diff --git a/packages/session-title/session-title-all-messages-llm/README.zh.md b/packages/session/session-title-all-messages-llm/README.zh.md similarity index 100% rename from packages/session-title/session-title-all-messages-llm/README.zh.md rename to packages/session/session-title-all-messages-llm/README.zh.md diff --git a/packages/session-title/session-title-all-messages-llm/package.json b/packages/session/session-title-all-messages-llm/package.json similarity index 100% rename from packages/session-title/session-title-all-messages-llm/package.json rename to packages/session/session-title-all-messages-llm/package.json diff --git a/packages/session-title/session-title-all-messages-llm/src/index.ts b/packages/session/session-title-all-messages-llm/src/index.ts similarity index 100% rename from packages/session-title/session-title-all-messages-llm/src/index.ts rename to packages/session/session-title-all-messages-llm/src/index.ts diff --git a/packages/session-title/session-title-all-messages-llm/src/invariant.ts b/packages/session/session-title-all-messages-llm/src/invariant.ts similarity index 100% rename from packages/session-title/session-title-all-messages-llm/src/invariant.ts rename to packages/session/session-title-all-messages-llm/src/invariant.ts diff --git a/packages/session-title/session-title-all-messages-llm/tests/provider.spec.ts b/packages/session/session-title-all-messages-llm/tests/provider.spec.ts similarity index 100% rename from packages/session-title/session-title-all-messages-llm/tests/provider.spec.ts rename to packages/session/session-title-all-messages-llm/tests/provider.spec.ts diff --git a/packages/session-title/session-title-all-messages-llm/tsconfig.json b/packages/session/session-title-all-messages-llm/tsconfig.json similarity index 100% rename from packages/session-title/session-title-all-messages-llm/tsconfig.json rename to packages/session/session-title-all-messages-llm/tsconfig.json diff --git a/packages/session-title/session-title-first-message-llm/README.i18n.yaml b/packages/session/session-title-first-message-llm/README.i18n.yaml similarity index 100% rename from packages/session-title/session-title-first-message-llm/README.i18n.yaml rename to packages/session/session-title-first-message-llm/README.i18n.yaml diff --git a/packages/session-title/session-title-first-message-llm/README.md b/packages/session/session-title-first-message-llm/README.md similarity index 100% rename from packages/session-title/session-title-first-message-llm/README.md rename to packages/session/session-title-first-message-llm/README.md diff --git a/packages/session-title/session-title-first-message-llm/README.zh.md b/packages/session/session-title-first-message-llm/README.zh.md similarity index 100% rename from packages/session-title/session-title-first-message-llm/README.zh.md rename to packages/session/session-title-first-message-llm/README.zh.md diff --git a/packages/session-title/session-title-first-message-llm/package.json b/packages/session/session-title-first-message-llm/package.json similarity index 100% rename from packages/session-title/session-title-first-message-llm/package.json rename to packages/session/session-title-first-message-llm/package.json diff --git a/packages/session-title/session-title-first-message-llm/src/index.ts b/packages/session/session-title-first-message-llm/src/index.ts similarity index 100% rename from packages/session-title/session-title-first-message-llm/src/index.ts rename to packages/session/session-title-first-message-llm/src/index.ts diff --git a/packages/session-title/session-title-first-message-llm/src/invariant.ts b/packages/session/session-title-first-message-llm/src/invariant.ts similarity index 100% rename from packages/session-title/session-title-first-message-llm/src/invariant.ts rename to packages/session/session-title-first-message-llm/src/invariant.ts diff --git a/packages/session-title/session-title-first-message-llm/tests/loader-composition.spec.ts b/packages/session/session-title-first-message-llm/tests/loader-composition.spec.ts similarity index 100% rename from packages/session-title/session-title-first-message-llm/tests/loader-composition.spec.ts rename to packages/session/session-title-first-message-llm/tests/loader-composition.spec.ts diff --git a/packages/session-title/session-title-first-message-llm/tests/provider.e2e.ts b/packages/session/session-title-first-message-llm/tests/provider.e2e.ts similarity index 100% rename from packages/session-title/session-title-first-message-llm/tests/provider.e2e.ts rename to packages/session/session-title-first-message-llm/tests/provider.e2e.ts diff --git a/packages/session-title/session-title-first-message-llm/tests/provider.spec.ts b/packages/session/session-title-first-message-llm/tests/provider.spec.ts similarity index 100% rename from packages/session-title/session-title-first-message-llm/tests/provider.spec.ts rename to packages/session/session-title-first-message-llm/tests/provider.spec.ts diff --git a/packages/session-title/session-title-first-message-llm/tsconfig.json b/packages/session/session-title-first-message-llm/tsconfig.json similarity index 100% rename from packages/session-title/session-title-first-message-llm/tsconfig.json rename to packages/session/session-title-first-message-llm/tsconfig.json diff --git a/packages/session-title/session-title-llm/README.i18n.yaml b/packages/session/session-title-llm/README.i18n.yaml similarity index 100% rename from packages/session-title/session-title-llm/README.i18n.yaml rename to packages/session/session-title-llm/README.i18n.yaml diff --git a/packages/session-title/session-title-llm/README.md b/packages/session/session-title-llm/README.md similarity index 100% rename from packages/session-title/session-title-llm/README.md rename to packages/session/session-title-llm/README.md diff --git a/packages/session-title/session-title-llm/README.zh.md b/packages/session/session-title-llm/README.zh.md similarity index 100% rename from packages/session-title/session-title-llm/README.zh.md rename to packages/session/session-title-llm/README.zh.md diff --git a/packages/session-title/session-title-llm/package.json b/packages/session/session-title-llm/package.json similarity index 100% rename from packages/session-title/session-title-llm/package.json rename to packages/session/session-title-llm/package.json diff --git a/packages/session-title/session-title-llm/src/index.ts b/packages/session/session-title-llm/src/index.ts similarity index 100% rename from packages/session-title/session-title-llm/src/index.ts rename to packages/session/session-title-llm/src/index.ts diff --git a/packages/session-title/session-title-llm/src/invariant.ts b/packages/session/session-title-llm/src/invariant.ts similarity index 100% rename from packages/session-title/session-title-llm/src/invariant.ts rename to packages/session/session-title-llm/src/invariant.ts diff --git a/packages/session-title/session-title-llm/tests/llm.spec.ts b/packages/session/session-title-llm/tests/llm.spec.ts similarity index 100% rename from packages/session-title/session-title-llm/tests/llm.spec.ts rename to packages/session/session-title-llm/tests/llm.spec.ts diff --git a/packages/session-title/session-title-llm/tsconfig.json b/packages/session/session-title-llm/tsconfig.json similarity index 100% rename from packages/session-title/session-title-llm/tsconfig.json rename to packages/session/session-title-llm/tsconfig.json diff --git a/packages/session-persistence/README.i18n.yaml b/packages/session/session-title/README.i18n.yaml similarity index 70% rename from packages/session-persistence/README.i18n.yaml rename to packages/session/session-title/README.i18n.yaml index 85eb2bd27d..f45602ce97 100644 --- a/packages/session-persistence/README.i18n.yaml +++ b/packages/session/session-title/README.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 packages/session-persistence/README.md -README.md: 060f757b3568318b0be4b01b3a10d019d28eac3b -README.zh.md: 67022565f7dabcbbccee30d5ce60fa559088ca39 +# pnpm run verify-translation-pairing --write packages/session/session-title/README.md +README.md: 13a2a1c298c3abb8c8987b9373edc46a071ce9db +README.zh.md: 9e271c044033ce17b5c8c744fe7a9cf433c7f473 diff --git a/packages/session-title/session-title/README.md b/packages/session/session-title/README.md similarity index 95% rename from packages/session-title/session-title/README.md rename to packages/session/session-title/README.md index 9a5ec27c36..13a2a1c298 100644 --- a/packages/session-title/session-title/README.md +++ b/packages/session/session-title/README.md @@ -31,7 +31,7 @@ All limits are required; the library supplies no defaults. A provider supplies a branded stable id, automatic mode (`first-message` or `all-user-messages`), and `generate(request)`. The request carries the live session, all eligible messages through one fixed revision, the current logged main-request route when available, and cancellation. The result identifies a non-empty title, unique ordered source-message seqs from that request, and optional model provenance. The service normalizes and validates the result before it becomes durable. -See the [session-title data structures](../../../docs/core-data-structures/session-title.md) and [implemented decision](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md). +See the [session-title data structures](../../../docs/subsystems/session-title.md) and [implemented decision](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md). ## Model Experience diff --git a/packages/session-title/session-title/README.zh.md b/packages/session/session-title/README.zh.md similarity index 95% rename from packages/session-title/session-title/README.zh.md rename to packages/session/session-title/README.zh.md index 3960be40e7..9e271c0440 100644 --- a/packages/session-title/session-title/README.zh.md +++ b/packages/session/session-title/README.zh.md @@ -31,7 +31,7 @@ Fork 出的会话会原样继承种子中的标题事件。首消息节奏不会 提供方会提供带品牌类型的稳定 id、自动模式(`first-message` 或 `all-user-messages`)和 `generate(request)`。请求携带活跃会话、截至一次固定修订的所有符合条件消息、可用时当前已记录的主请求路由,以及取消信号。结果包含非空标题、该请求中唯一且有序的来源消息 seq,以及可选的模型来源信息。服务会在结果持久保存前进行规范化和验证。 -参见[会话标题数据结构](../../../docs/core-data-structures/session-title.md)与[已实现决策](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)。 +参见[会话标题数据结构](../../../docs/subsystems/session-title.md)与[已实现决策](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)。 ## 模型体验 diff --git a/packages/session-title/session-title/package.json b/packages/session/session-title/package.json similarity index 100% rename from packages/session-title/session-title/package.json rename to packages/session/session-title/package.json diff --git a/packages/session-title/session-title/src/client.ts b/packages/session/session-title/src/client.ts similarity index 100% rename from packages/session-title/session-title/src/client.ts rename to packages/session/session-title/src/client.ts diff --git a/packages/session-title/session-title/src/index.ts b/packages/session/session-title/src/index.ts similarity index 100% rename from packages/session-title/session-title/src/index.ts rename to packages/session/session-title/src/index.ts diff --git a/packages/session-title/session-title/src/invariant.ts b/packages/session/session-title/src/invariant.ts similarity index 100% rename from packages/session-title/session-title/src/invariant.ts rename to packages/session/session-title/src/invariant.ts diff --git a/packages/session-title/session-title/src/normalize.ts b/packages/session/session-title/src/normalize.ts similarity index 100% rename from packages/session-title/session-title/src/normalize.ts rename to packages/session/session-title/src/normalize.ts diff --git a/packages/session-title/session-title/src/types.ts b/packages/session/session-title/src/types.ts similarity index 100% rename from packages/session-title/session-title/src/types.ts rename to packages/session/session-title/src/types.ts diff --git a/packages/session-title/session-title/tests/invariant.spec.ts b/packages/session/session-title/tests/invariant.spec.ts similarity index 100% rename from packages/session-title/session-title/tests/invariant.spec.ts rename to packages/session/session-title/tests/invariant.spec.ts diff --git a/packages/session-title/session-title/tests/persistence.spec.ts b/packages/session/session-title/tests/persistence.spec.ts similarity index 100% rename from packages/session-title/session-title/tests/persistence.spec.ts rename to packages/session/session-title/tests/persistence.spec.ts diff --git a/packages/session-title/session-title/tests/projection.spec.ts b/packages/session/session-title/tests/projection.spec.ts similarity index 100% rename from packages/session-title/session-title/tests/projection.spec.ts rename to packages/session/session-title/tests/projection.spec.ts diff --git a/packages/session-title/session-title/tests/provider.spec.ts b/packages/session/session-title/tests/provider.spec.ts similarity index 100% rename from packages/session-title/session-title/tests/provider.spec.ts rename to packages/session/session-title/tests/provider.spec.ts diff --git a/packages/session-title/session-title/tests/rename.spec.ts b/packages/session/session-title/tests/rename.spec.ts similarity index 100% rename from packages/session-title/session-title/tests/rename.spec.ts rename to packages/session/session-title/tests/rename.spec.ts diff --git a/packages/session-title/session-title/tests/service-contracts.spec.ts b/packages/session/session-title/tests/service-contracts.spec.ts similarity index 100% rename from packages/session-title/session-title/tests/service-contracts.spec.ts rename to packages/session/session-title/tests/service-contracts.spec.ts diff --git a/packages/session-title/session-title/tests/session-title.spec.ts b/packages/session/session-title/tests/session-title.spec.ts similarity index 100% rename from packages/session-title/session-title/tests/session-title.spec.ts rename to packages/session/session-title/tests/session-title.spec.ts diff --git a/packages/session-title/session-title/tsconfig.json b/packages/session/session-title/tsconfig.json similarity index 90% rename from packages/session-title/session-title/tsconfig.json rename to packages/session/session-title/tsconfig.json index 80aef8bbfb..ca7c8e65dd 100644 --- a/packages/session-title/session-title/tsconfig.json +++ b/packages/session/session-title/tsconfig.json @@ -30,7 +30,7 @@ "path": "../../core/session" }, { - "path": "../../session-projection/session-projection" + "path": "../session-projection" } ] } diff --git a/packages/settings/README.i18n.yaml b/packages/settings/README.i18n.yaml index ec2d2124a3..146f0ad32e 100644 --- a/packages/settings/README.i18n.yaml +++ b/packages/settings/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/settings/README.md -README.md: 3f43647f0558abf53c373ef7d97af16c0e17d2fe -README.zh.md: b5779bfe5da4cae148bcc6515ef13790f78bca7a +README.md: 6f3719c58564a90fec3bd9f305cfaeb2b7bf0bf4 +README.zh.md: 0efb16dd4e5d5faa5f16814b94d55a20dc4c06c3 diff --git a/packages/settings/README.md b/packages/settings/README.md index 3f43647f05..6f3719c585 100644 --- a/packages/settings/README.md +++ b/packages/settings/README.md @@ -8,3 +8,5 @@ This family resolves user-editable configuration through registered namespaces a |---|---|---| | [`settings/`](settings/README.md) | Defines namespace registration, layered resolution, and commits | `ctx.settings` | | [`settings-local/`](settings-local/README.md) | Stores settings in a local file and observes external edits | registers on `ctx.settings` | + +The subsystem reference — namespaces, owner scopes, resolution order, hot commits — is [docs/subsystems/settings.md](../../docs/subsystems/settings.md). diff --git a/packages/settings/README.zh.md b/packages/settings/README.zh.md index b5779bfe5d..0efb16dd4e 100644 --- a/packages/settings/README.zh.md +++ b/packages/settings/README.zh.md @@ -8,3 +8,5 @@ |---|---|---| | [`settings/`](settings/README.md) | 定义命名空间注册、分层解析与提交 | `ctx.settings` | | [`settings-local/`](settings-local/README.md) | 在本地文件中存储设置并观察外部编辑 | 注册到 `ctx.settings` | + +子系统参考——命名空间、owner scope、解析顺序、热提交——见 [docs/subsystems/settings.md](../../docs/subsystems/settings.md)。 diff --git a/packages/skill/README.i18n.yaml b/packages/skill/README.i18n.yaml index 74875f2aa3..758d0552f9 100644 --- a/packages/skill/README.i18n.yaml +++ b/packages/skill/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/skill/README.md -README.md: 533904859ad998de4f371a073fde98b68660097b -README.zh.md: 1fad581cc61a05251f671577dcb7edab37281283 +README.md: cd606e778335249bf8ecc876d3b1a4e5feb79448 +README.zh.md: 4db7e2524596f4c62144ec7eabe7c6af63e2e270 diff --git a/packages/skill/README.md b/packages/skill/README.md index 533904859a..cd606e7783 100644 --- a/packages/skill/README.md +++ b/packages/skill/README.md @@ -12,3 +12,5 @@ This family discovers reusable agent instructions and exposes them to the model | [`tool-skill/`](tool-skill/README.md) | Publishes the skill catalog and model-facing loader | registers on `ctx.tools` | This capability remains outside the core control spine and can use local, embedded, or remote providers without changing the model-facing contract. + +The subsystem reference — discovery priority, catalog snapshots, the `skill` loader — is [docs/subsystems/skills.md](../../docs/subsystems/skills.md). diff --git a/packages/skill/README.zh.md b/packages/skill/README.zh.md index 1fad581cc6..4db7e25245 100644 --- a/packages/skill/README.zh.md +++ b/packages/skill/README.zh.md @@ -12,3 +12,5 @@ | [`tool-skill/`](tool-skill/README.md) | 发布 skill 目录和面向模型的 loader | 注册到 `ctx.tools` | 该能力位于核心控制主干之外,可以使用本地、嵌入式或远程提供方,而无需更改面向模型的契约。 + +子系统参考——发现优先级、目录快照、`skill` 加载器——见 [docs/subsystems/skills.md](../../docs/subsystems/skills.md)。 diff --git a/packages/spill/README.i18n.yaml b/packages/spill/README.i18n.yaml index 95800ac03e..62ed26a59b 100644 --- a/packages/spill/README.i18n.yaml +++ b/packages/spill/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/spill/README.md -README.md: b96375b1a332cd6e3e4da9c8b1c98011aa97c8bc -README.zh.md: c8495068edf1035937c99198dfdb5db65c9a9e59 +README.md: 74fd4837cb23767754174df168f2f4ba71c2b938 +README.zh.md: 16d677719a9c3f4245a2f446017641ff6b50ffa8 diff --git a/packages/spill/README.md b/packages/spill/README.md index b96375b1a3..74fd4837cb 100644 --- a/packages/spill/README.md +++ b/packages/spill/README.md @@ -11,3 +11,5 @@ This family persists oversized tool output and replaces the inline result with a | [`spill-policy/`](spill-policy/README.md) | Applies the post-execution spill policy | listens on `ctx.tools` | See the [tool-output spill decision](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md) for the boundary between storage, retention, and tool-owned output handling. + +The subsystem reference — `SaveTextSpill`, owners/sources, the branded locator — is [docs/subsystems/spill.md](../../docs/subsystems/spill.md); rationale in the [tool output spill Agent Note](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md). diff --git a/packages/spill/README.zh.md b/packages/spill/README.zh.md index c8495068ed..16d677719a 100644 --- a/packages/spill/README.zh.md +++ b/packages/spill/README.zh.md @@ -11,3 +11,5 @@ | [`spill-policy/`](spill-policy/README.md) | 应用执行后 spill 策略 | 监听 `ctx.tools` | 参见[工具输出 spill 决策](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md),其中记录了存储、保留和工具自有输出处理之间的边界。 + +子系统参考——`SaveTextSpill`、owner/来源、品牌化定位符——见 [docs/subsystems/spill.md](../../docs/subsystems/spill.md);依据见[工具输出 spill Agent Note](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md)。 diff --git a/packages/storage/README.i18n.yaml b/packages/storage/README.i18n.yaml index 58fc19a4f8..ef32481991 100644 --- a/packages/storage/README.i18n.yaml +++ b/packages/storage/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/storage/README.md -README.md: a5f9d0204b93445699d28b1285a4a18f0408930e -README.zh.md: 0710dfb57f96011755b253ad95a0f9d2c0923a70 +README.md: a178495fca5cc433182b8e75f20d479cb7c5bc99 +README.zh.md: 1a08f1676e5c699a55f9956a42059d1f5acf7e8d diff --git a/packages/storage/README.md b/packages/storage/README.md index a5f9d0204b..a178495fca 100644 --- a/packages/storage/README.md +++ b/packages/storage/README.md @@ -12,3 +12,5 @@ This family persists application data other than session event logs through name | [`storage-domain/`](storage-domain/README.md) | Provides validated domain-record storage | `ctx.storageDomain` | Consumers use a data form rather than accessing a backend directly. The [domain storage decision](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md) records the family design. + +The subsystem reference — the backend seam, `StorageForms`, `DomainSpec`/`Domain`, `domain/changed` — is [docs/subsystems/storage.md](../../docs/subsystems/storage.md). diff --git a/packages/storage/README.zh.md b/packages/storage/README.zh.md index 0710dfb57f..1a08f1676e 100644 --- a/packages/storage/README.zh.md +++ b/packages/storage/README.zh.md @@ -12,3 +12,5 @@ | [`storage-domain/`](storage-domain/README.md) | 提供经过验证的领域记录存储 | `ctx.storageDomain` | 消费方使用数据形式,而不是直接访问后端。[领域存储决策](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md)记录了该家族的设计。 + +子系统参考——后端 seam、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed`——见 [docs/subsystems/storage.md](../../docs/subsystems/storage.md)。 diff --git a/packages/subagent/README.i18n.yaml b/packages/subagent/README.i18n.yaml index 11357c76d2..fe76e74a77 100644 --- a/packages/subagent/README.i18n.yaml +++ b/packages/subagent/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/subagent/README.md -README.md: 0a342569e66539e4987710b2e56f2946c97b1ac1 -README.zh.md: 5d2f7beef478b8bfd27b4772c7a951ea62cb10ef +README.md: c8969ad313fc673456d2740b40d90c6d8db794ac +README.zh.md: fb18afba5d8680408f0f37ab75056a3fb0ef660a diff --git a/packages/subagent/README.md b/packages/subagent/README.md index 0a342569e6..c8969ad313 100644 --- a/packages/subagent/README.md +++ b/packages/subagent/README.md @@ -19,3 +19,5 @@ This family lets an agent delegate work to child agents. Multiple named provider | [`tool-subagent-report/`](tool-subagent-report/README.md) | Provides the child-to-parent report channel | registers in child scopes | See the decisions for the [capability family](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [continuable children](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), and [control tools](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md). + +The subsystem reference — start requests, results, live runs, the provider seam, continuable background children — is [docs/subsystems/subagent.md](../../docs/subsystems/subagent.md); design rationale in the [subagent capability seam](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [continuable background subagents](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), and [merged subagent control service](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md) Agent Notes. diff --git a/packages/subagent/README.zh.md b/packages/subagent/README.zh.md index 5d2f7beef4..fb18afba5d 100644 --- a/packages/subagent/README.zh.md +++ b/packages/subagent/README.zh.md @@ -19,3 +19,5 @@ | [`tool-subagent-report/`](tool-subagent-report/README.md) | 提供从子级到父级的报告通道 | 注册到子级作用域 | 参见[能力家族](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续执行的子级](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)和[控制工具](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)决策。 + +子系统参考——启动请求、结果、实时运行、提供方 seam、可续跑后台子 agent——见 [docs/subsystems/subagent.md](../../docs/subsystems/subagent.md);设计依据见 [subagent 能力 seam](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可续跑后台 subagent](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md) 与[合并 subagent 控制服务](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md) Agent Note。 diff --git a/packages/subagent/subagent-codex/tsconfig.json b/packages/subagent/subagent-codex/tsconfig.json index b9f33967ba..3bb974477e 100644 --- a/packages/subagent/subagent-codex/tsconfig.json +++ b/packages/subagent/subagent-codex/tsconfig.json @@ -24,7 +24,7 @@ "path": "../../llm/llm" }, { - "path": "../../sdk/sdk-protocol" + "path": "../../scaffold/protocol" }, { "path": "../../core/session" diff --git a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml b/packages/subagent/subagent-dsh-sdk/README.i18n.yaml index 3598c77d31..246d702892 100644 --- a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml +++ b/packages/subagent/subagent-dsh-sdk/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/subagent/subagent-dsh-sdk/README.md -README.md: c0e2f4e9ece366e28492e32d97afa533fd948141 -README.zh.md: 4cf6c58fff44b1c11cf0a6c321da2c02d52bb8c9 +README.md: 0bbcfa105ecf024a2492d39d3bf8d28956110050 +README.zh.md: ffb1b8f4cddca4566a20593bdac54801ad8db38f diff --git a/packages/subagent/subagent-dsh-sdk/README.md b/packages/subagent/subagent-dsh-sdk/README.md index c0e2f4e9ec..0bbcfa105e 100644 --- a/packages/subagent/subagent-dsh-sdk/README.md +++ b/packages/subagent/subagent-dsh-sdk/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The SDK provider runs each subagent as a complete DeepSeek Harness runtime in a fresh subprocess, driven over stdio JSON-RPC through the [TypeScript SDK client](../../sdk/sdk-client/README.md). It is the second out-of-process backend beside [`subagent-acp`](../subagent-acp/README.md), differing in the wire and the child contract: the ACP backend drives any Agent Client Protocol agent; this backend drives specifically a harness SDK runtime (`dsh-jsonrpc-agent` bin or packaged executable), so the child is a full peer harness — own `cordis.yml`-decided composition, session persistence, model route, and tools. +The SDK provider runs each subagent as a complete DeepSeek Harness runtime in a fresh subprocess, driven over stdio JSON-RPC through the [TypeScript SDK client](../../scaffold/client/README.md). It is the second out-of-process backend beside [`subagent-acp`](../subagent-acp/README.md), differing in the wire and the child contract: the ACP backend drives any Agent Client Protocol agent; this backend drives specifically a harness SDK runtime (`dsh-jsonrpc-agent` bin or packaged executable), so the child is a full peer harness — own `cordis.yml`-decided composition, session persistence, model route, and tools. ## Start and ownership diff --git a/packages/subagent/subagent-dsh-sdk/README.zh.md b/packages/subagent/subagent-dsh-sdk/README.zh.md index 4cf6c58fff..ffb1b8f4cd 100644 --- a/packages/subagent/subagent-dsh-sdk/README.zh.md +++ b/packages/subagent/subagent-dsh-sdk/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -SDK 提供方会在全新的子进程中把每个 subagent 作为完整的 DeepSeek Harness 运行时运行,并经由 [TypeScript SDK 客户端](../../sdk/sdk-client/README.md) 通过 stdio JSON-RPC 驱动。它是 [`subagent-acp`](../subagent-acp/README.md) 之外的第二个进程外后端,差异在协议格式(wire format)和子进程契约:ACP(Agent Client Protocol)后端能驱动任何 Agent Client Protocol agent(智能体);本后端专门驱动 harness SDK 运行时(`dsh-jsonrpc-agent` bin 或打包后的可执行文件),因此子进程是一个完整的对等 harness,拥有由 `cordis.yml` 决定的组合、会话持久化、模型路由和工具。 +SDK 提供方会在全新的子进程中把每个 subagent 作为完整的 DeepSeek Harness 运行时运行,并经由 [TypeScript SDK 客户端](../../scaffold/client/README.md) 通过 stdio JSON-RPC 驱动。它是 [`subagent-acp`](../subagent-acp/README.md) 之外的第二个进程外后端,差异在协议格式(wire format)和子进程契约:ACP(Agent Client Protocol)后端能驱动任何 Agent Client Protocol agent(智能体);本后端专门驱动 harness SDK 运行时(`dsh-jsonrpc-agent` bin 或打包后的可执行文件),因此子进程是一个完整的对等 harness,拥有由 `cordis.yml` 决定的组合、会话持久化、模型路由和工具。 ## 启动与所有权 diff --git a/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts b/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts index 5603e0e9a0..e42a76c90c 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts @@ -24,7 +24,7 @@ import { type SdkRunSpec, } from '../src/run.ts' -const fakeRuntime = fileURLToPath(new URL('../../../sdk/sdk-client/tests/fake-runtime.ts', import.meta.url)) +const fakeRuntime = fileURLToPath(new URL('../../../scaffold/client/tests/fake-runtime.ts', import.meta.url)) /** A parent Agent stub. The SDK backend reads exactly one thing off it: the session header's cwd (the workspace its child inherits). */ const fakeParent = { id: 'parent', session: { header: { cwd: process.cwd() } } } as unknown as Agent diff --git a/packages/subagent/subagent-dsh-sdk/tsconfig.json b/packages/subagent/subagent-dsh-sdk/tsconfig.json index 79ff03f1a4..2b9aa29e21 100644 --- a/packages/subagent/subagent-dsh-sdk/tsconfig.json +++ b/packages/subagent/subagent-dsh-sdk/tsconfig.json @@ -27,10 +27,10 @@ "path": "../../core/session" }, { - "path": "../../sdk/sdk-client" + "path": "../../scaffold/client" }, { - "path": "../../sdk/sdk-protocol" + "path": "../../scaffold/protocol" }, { "path": "../subagent" diff --git a/packages/subagent/subagent-inprocess/tsconfig.json b/packages/subagent/subagent-inprocess/tsconfig.json index 22c00b3a94..23406e362e 100644 --- a/packages/subagent/subagent-inprocess/tsconfig.json +++ b/packages/subagent/subagent-inprocess/tsconfig.json @@ -39,7 +39,7 @@ "path": "../../support/invariants" }, { - "path": "../../ui/user-approval" + "path": "../../interaction/user-approval" } ] } diff --git a/packages/subagent/subagent/tsconfig.json b/packages/subagent/subagent/tsconfig.json index de2fff3d84..c72f2ef68d 100644 --- a/packages/subagent/subagent/tsconfig.json +++ b/packages/subagent/subagent/tsconfig.json @@ -27,13 +27,13 @@ "path": "../../core/scope" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { - "path": "../../session-projection/session-projection" + "path": "../../session/session-projection" }, { - "path": "../../session-projection/session-projection-cache" + "path": "../../session/session-projection-cache" }, { "path": "../../tasks/tasks" diff --git a/packages/subprocess/README.i18n.yaml b/packages/subprocess/README.i18n.yaml index 0ebb5bd4af..2088cbdb6b 100644 --- a/packages/subprocess/README.i18n.yaml +++ b/packages/subprocess/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/subprocess/README.md -README.md: f2b19436da40feb14d067e2cfc706222625680b5 -README.zh.md: 938312448dd5c0a691ed07ddc9843cf2c4445637 +README.md: 9a8628e2b6d16852895be33ae516c8b33b078502 +README.zh.md: d2c539fb1c66a914aa5f758d928fba01b5600d15 diff --git a/packages/subprocess/README.md b/packages/subprocess/README.md index f2b19436da..9a8628e2b6 100644 --- a/packages/subprocess/README.md +++ b/packages/subprocess/README.md @@ -10,3 +10,5 @@ The shared process substrate for one execution world: executable lookup, fully-s | [`subprocess-local`](subprocess-local/README.md) (`@deepseek-ai/dsh-subprocess-local`) | — | The local implementation: detached process trees, bounded collection/spill, `node-pty`, foreground/session inspection, tree signalling, and terminate-and-join disposal | The service owns process lifetime across consumer reloads; consumers own what a process means (a bash command, a future non-shell runner) and every default that shapes one. + +The subsystem reference — spawn specs, output readers, outcomes, the `DSH_*` environment — is [docs/subsystems/subprocess.md](../../docs/subsystems/subprocess.md); the seam decision in the [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md). diff --git a/packages/subprocess/README.zh.md b/packages/subprocess/README.zh.md index 938312448d..d2c539fb1c 100644 --- a/packages/subprocess/README.zh.md +++ b/packages/subprocess/README.zh.md @@ -10,3 +10,5 @@ | [`subprocess-local`](subprocess-local/README.md)(`@deepseek-ai/dsh-subprocess-local`) | 无 | 本地实现:detached 进程树、有界收集/spill、`node-pty`、前台/会话检查、进程树信号发送,以及先终止再等待退出的资源释放 | 即使消费方重载,进程生命周期仍由服务负责管理;消费方负责定义进程的含义(一条 bash 命令、未来的非 shell 运行器),以及决定塑造该进程的每一项默认值。 + +子系统参考——spawn spec、输出读取器、结果、`DSH_*` 环境——见 [docs/subsystems/subprocess.md](../../docs/subsystems/subprocess.md);seam 决定见 [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。 diff --git a/packages/subprocess/subprocess/README.i18n.yaml b/packages/subprocess/subprocess/README.i18n.yaml index e67bf1a87f..7a33a31d0c 100644 --- a/packages/subprocess/subprocess/README.i18n.yaml +++ b/packages/subprocess/subprocess/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/subprocess/subprocess/README.md -README.md: ec4a4e3328a5a600441d2e7983b3844f4ccc5e91 -README.zh.md: 3da79995fad9a2c8f2a6020ae18152a0e4459c71 +README.md: c28361f5e767f81fe8d1ca4757aece83dccfea5c +README.zh.md: 3ad73bdea3b137f1c9139fdb1b52c49eb10b5500 diff --git a/packages/subprocess/subprocess/README.md b/packages/subprocess/subprocess/README.md index ec4a4e3328..c28361f5e7 100644 --- a/packages/subprocess/subprocess/README.md +++ b/packages/subprocess/subprocess/README.md @@ -15,7 +15,7 @@ The subprocess seam (`ctx.subprocess`) is the process half of one execution worl - `scrubbedParentEnv()` / `SENSITIVE_ENV_PATTERN` are the one shared scrub definition: ambient credential-shaped and `DSH_*` names are dropped, and explicit `env` merges after the scrub. The local ordinary and terminal spawns both apply it; SDK-managed transports that own their spawn may import it directly. - Disposal of the service terminates all still-running managed processes and awaits their exit. -See the [subprocess data-structure catalog](../../../docs/core-data-structures/subprocess.md) and the [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md). +See the [subprocess subsystem page](../../../docs/subsystems/subprocess.md) and the [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md). ## Model Experience diff --git a/packages/subprocess/subprocess/README.zh.md b/packages/subprocess/subprocess/README.zh.md index 3da79995fa..3ad73bdea3 100644 --- a/packages/subprocess/subprocess/README.zh.md +++ b/packages/subprocess/subprocess/README.zh.md @@ -15,7 +15,7 @@ - `scrubbedParentEnv()` / `SENSITIVE_ENV_PATTERN` 是唯一一份共享的环境清理定义:环境中形似凭据的名称与 `DSH_*` 名称都会被丢弃,显式 `env` 在清除之后合并。本地的普通 spawn 与终端 spawn 都应用该定义;拥有自身 spawn 的 SDK 管理传输可直接导入它。 - 服务自身的 dispose(资源释放)会终止所有仍在运行的受管进程并等待其退出。 -参见[子进程数据结构目录](../../../docs/core-data-structures/subprocess.md)与[seam Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。 +参见[子进程子系统页面](../../../docs/subsystems/subprocess.md)与[seam Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。 ## 模型体验 diff --git a/packages/support/README.i18n.yaml b/packages/support/README.i18n.yaml index 1b392a1729..824f20ae7e 100644 --- a/packages/support/README.i18n.yaml +++ b/packages/support/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/support/README.md -README.md: 15cb82d3d76a1241af8b40e1c8536292618409a9 -README.zh.md: ff8595b47f1a1220912dc77a072463adf74f7bf9 +README.md: bf0bba1ea7d21700a6743195ec966512c20e01bc +README.zh.md: 47eba4d51c66914f388ba4a5925323173d5ed7e2 diff --git a/packages/support/README.md b/packages/support/README.md index 15cb82d3d7..bf0bba1ea7 100644 --- a/packages/support/README.md +++ b/packages/support/README.md @@ -14,3 +14,5 @@ These packages support repository development, tests, and examples rather than p | [`llm-replay/`](llm-replay/README.md) | Replays recorded model responses for keyless tests and demos | A package moves out of `support/` when it gains a product contract and product consumers. + +The invariants contract is documented in [docs/subsystems/invariants.md](../../docs/subsystems/invariants.md). diff --git a/packages/support/README.zh.md b/packages/support/README.zh.md index ff8595b47f..47eba4d51c 100644 --- a/packages/support/README.zh.md +++ b/packages/support/README.zh.md @@ -14,3 +14,5 @@ | [`llm-replay/`](llm-replay/README.md) | 为无密钥测试和演示回放已记录的模型响应 | 当一个包获得产品契约和产品消费方时,它会移出 `support/`。 + +不变式契约记录在 [docs/subsystems/invariants.md](../../docs/subsystems/invariants.md)。 diff --git a/packages/tasks/README.i18n.yaml b/packages/tasks/README.i18n.yaml index 38cd47299c..d5df91be62 100644 --- a/packages/tasks/README.i18n.yaml +++ b/packages/tasks/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/tasks/README.md -README.md: 05ed9c439684337c45abe11f03c861a1b795b2ac -README.zh.md: 16fe6c0a2b183b719cb6aa8ac068e34d51832a2b +README.md: e0b6769cd1d28cc7159310a6ec6f5c8d94a81a24 +README.zh.md: d030081470a503fb52fd72dcc6f89f4c535dc38c diff --git a/packages/tasks/README.md b/packages/tasks/README.md index 05ed9c4396..e0b6769cd1 100644 --- a/packages/tasks/README.md +++ b/packages/tasks/README.md @@ -11,3 +11,5 @@ This family gives long-running tools one owner-isolated background-task protocol | [`tool-tasks/`](tool-tasks/README.md) | Exposes task control and completion notices to the model | registers on `ctx.tools` | See the [background-task runtime](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and [task-registry](../../.agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md) decisions. + +The subsystem reference — the id scheme, the owner-fenced contract, snapshots — is [docs/subsystems/tasks.md](../../docs/subsystems/tasks.md); design in the [background-task runtime](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and [task-registry seam](../../.agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md) Agent Notes. diff --git a/packages/tasks/README.zh.md b/packages/tasks/README.zh.md index 16fe6c0a2b..d030081470 100644 --- a/packages/tasks/README.zh.md +++ b/packages/tasks/README.zh.md @@ -11,3 +11,5 @@ | [`tool-tasks/`](tool-tasks/README.md) | 向模型公开任务控制和完成通知 | 注册到 `ctx.tools` | 参见[后台任务运行时](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)和[任务注册表](../../.agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md)决策。 + +子系统参考——id 方案、拥有者围栏契约、快照——见 [docs/subsystems/tasks.md](../../docs/subsystems/tasks.md);设计见[后台任务运行时](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与[任务注册表 seam](../../.agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md) Agent Note。 diff --git a/packages/tasks/tasks/README.i18n.yaml b/packages/tasks/tasks/README.i18n.yaml index 7ceb8ff432..1d803e486e 100644 --- a/packages/tasks/tasks/README.i18n.yaml +++ b/packages/tasks/tasks/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/tasks/tasks/README.md -README.md: 2f822bad139020f0ebae0165aa4e8893853f635d -README.zh.md: fdc619fbb46267b2ae550c85cb14fcf8a916f638 +README.md: b5a7380e0cb7df5dd20baecea8656db2aad3460a +README.zh.md: f2e4b10d30d81c840f5598141bbe246634bf2884 diff --git a/packages/tasks/tasks/README.md b/packages/tasks/tasks/README.md index 2f822bad13..b5a7380e0c 100644 --- a/packages/tasks/tasks/README.md +++ b/packages/tasks/tasks/README.md @@ -20,7 +20,7 @@ Owned access compares the task's `SessionId` with the caller's. Ids such as `bas Implementations also owe the lifecycle semantics of the contract: registrations outlive producer and control-surface fibers, owner and service disposal cancel live work and await compliant producers, and settlement is first-wins — one terminal record, one round of contained listener notification, released waiters. -See the [task type catalog](../../../docs/core-data-structures/tasks.md), the [runtime Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md), and the [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md). +See the [task type catalog](../../../docs/subsystems/tasks.md), the [runtime Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md), and the [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md). ## Model Experience diff --git a/packages/tasks/tasks/README.zh.md b/packages/tasks/tasks/README.zh.md index fdc619fbb4..f2e4b10d30 100644 --- a/packages/tasks/tasks/README.zh.md +++ b/packages/tasks/tasks/README.zh.md @@ -20,7 +20,7 @@ 实现还必须兑现契约的生命周期语义:注册的存续期长于生产方 fiber 与控制表层 fiber,owner 释放和服务释放会取消仍在运行的工作并等待守约的生产方,结算遵循首次结果优先(一条终止记录、一轮异常受到隔离的监听器通知,然后释放等待方)。 -参见[任务类型目录](../../../docs/core-data-structures/tasks.md)、[运行时 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)和 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md)。 +参见[任务类型目录](../../../docs/subsystems/tasks.md)、[运行时 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)和 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md)。 ## 模型体验 diff --git a/packages/telemetry/README.i18n.yaml b/packages/telemetry/README.i18n.yaml deleted file mode 100644 index 6f60d47612..0000000000 --- a/packages/telemetry/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 packages/telemetry/README.md -README.md: d1910323176738d1ecab8fe8e6c07a2811f0f5cd -README.zh.md: 229cd3acc5ce3dc49a9c862efa824f4ee6a1fa7b diff --git a/packages/telemetry/README.md b/packages/telemetry/README.md deleted file mode 100644 index d191032317..0000000000 --- a/packages/telemetry/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# telemetry/ — session telemetry capability family - -English | [中文](README.zh.md) - -This family projects session activity into outbound telemetry and delegates delivery to a configured reporting backend. The [telemetry decision](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md) records the reporting boundary; the [mode decision](../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md) records immediate, feedback-gated, and disabled delivery. - -| Package | Role | -|---|---| -| [`session-telemetry/`](session-telemetry/README.md) | Defines capture, redaction, projection, and live or on-demand backend delivery. | -| [`session-telemetry-otel/`](session-telemetry-otel/README.md) | Delivers telemetry through OpenTelemetry logs in `FULL`, `FEEDBACK_ONLY`, or `DISABLED` mode. | diff --git a/packages/telemetry/README.zh.md b/packages/telemetry/README.zh.md deleted file mode 100644 index 229cd3acc5..0000000000 --- a/packages/telemetry/README.zh.md +++ /dev/null @@ -1,10 +0,0 @@ -# telemetry/:会话遥测能力家族 - -[English](README.md) | 中文 - -本家族将会话活动投影为外发遥测,并将投递委派给配置的上报后端。[遥测决策](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)记录上报边界;[模式决策](../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md)记录即时、反馈门控与禁用投递。 - -| 包 | 职责 | -|---|---| -| [`session-telemetry/`](session-telemetry/README.md) | 定义捕获、脱敏、投影,以及实时或按需后端投递。 | -| [`session-telemetry-otel/`](session-telemetry-otel/README.md) | 通过 OpenTelemetry 日志以 `FULL`、`FEEDBACK_ONLY` 或 `DISABLED` 模式投递遥测。 | diff --git a/packages/timeout/README.md b/packages/timeout/README.md deleted file mode 100644 index d9d8e8a829..0000000000 --- a/packages/timeout/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# timeout/ — tool-call timeout policy - -English | [中文](README.zh.md) - -This group applies deployment-configured deadlines to model-facing tool calls. Capabilities remain responsible for terminating their own work. - -| Package | Role | -|---|---| -| [`timeout-policy/`](timeout-policy/README.md) | Enforces configured per-tool call deadlines | - -The pure timing primitives live in [`util/timeout`](../util/timeout/README.md). See the [timeout-library decision](../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md). diff --git a/packages/timeout/README.zh.md b/packages/timeout/README.zh.md deleted file mode 100644 index c5920258ad..0000000000 --- a/packages/timeout/README.zh.md +++ /dev/null @@ -1,11 +0,0 @@ -# timeout/:工具调用超时策略 - -[English](README.md) | 中文 - -本分组将部署配置的截止时间应用于面向模型的工具调用。各项能力仍负责终止自身工作。 - -| 包 | 职责 | -|---|---| -| [`timeout-policy/`](timeout-policy/README.md) | 强制执行配置的逐工具调用截止时间 | - -纯计时原语位于 [`util/timeout`](../util/timeout/README.md)。参见[超时库决策](../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)。 diff --git a/packages/todo/README.i18n.yaml b/packages/todo/README.i18n.yaml index c75e09350a..57a1538b78 100644 --- a/packages/todo/README.i18n.yaml +++ b/packages/todo/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/todo/README.md -README.md: 66abf18131ee53d87e933757c40117841e94b07f -README.zh.md: 38a6b3c22653b8e22cc0b693eb666620513a3d8c +README.md: 6c7dccbbfb007737593550cea91444f8f735cf81 +README.zh.md: 3c1c35187b5111f1fdb607d70b980be57e3e79c1 diff --git a/packages/todo/README.md b/packages/todo/README.md index 66abf18131..6c7dccbbfb 100644 --- a/packages/todo/README.md +++ b/packages/todo/README.md @@ -9,3 +9,5 @@ The model-facing todo capability. It is a single **product** package because one | [`tool-todo/`](tool-todo/README.md) | Stores and exposes the session's todo list. | (registers on `ctx.tools`) | The child README owns the tool, persistence, and rendering contract. + +The event payload is documented on [docs/subsystems/session.md](../../docs/subsystems/session.md). diff --git a/packages/todo/README.zh.md b/packages/todo/README.zh.md index 38a6b3c226..3c1c35187b 100644 --- a/packages/todo/README.zh.md +++ b/packages/todo/README.zh.md @@ -9,3 +9,5 @@ | [`tool-todo/`](tool-todo/README.md) | 存储并公开会话的 todo 列表。 | (注册到 `ctx.tools`) | 子级 README 负责工具、持久化和渲染契约。 + +事件载荷记录在 [docs/subsystems/session.md](../../docs/subsystems/session.md)。 diff --git a/packages/todo/tool-todo/README.i18n.yaml b/packages/todo/tool-todo/README.i18n.yaml index 20b8dffbbf..6aa521df26 100644 --- a/packages/todo/tool-todo/README.i18n.yaml +++ b/packages/todo/tool-todo/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/todo/tool-todo/README.md -README.md: 914e89a000e4bb87ebd7844f05db3809c6726528 -README.zh.md: c88dbf976fa5110028fcc964ab9aa8efcc3244d3 +README.md: 7c993ac6f7f0047d008a09e5ff52cb752f94763c +README.zh.md: 9cea66147beddab7d62ed7344cbe866495e28022 diff --git a/packages/todo/tool-todo/README.md b/packages/todo/tool-todo/README.md index 914e89a000..7c993ac6f7 100644 --- a/packages/todo/tool-todo/README.md +++ b/packages/todo/tool-todo/README.md @@ -30,7 +30,7 @@ The canonical result is `{ todos, counts: { pending, inProgress, completed } }`; ## Session projection -When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.md)), this package registers the `todos` projection unit under an injected child: `init` = `null` (no write yet), `apply` = take the whole list from each `todo/write` and clear to `null` on each `turn/start` (standing plan; `turn/end` keeps the finished checklist; every other event returns the same state reference), `view` = identity, `stateVersion` = 2. The key merges into `SessionProjectionMap` here (via the interface package's `/types` outlet); the framework drives the unit and carriers serve the value on the history tail page and the `session/projection` push frame. Compositions without the registry are unaffected. Lifetime rationale: [todo plan clears on next turn](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md). +When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md)), this package registers the `todos` projection unit under an injected child: `init` = `null` (no write yet), `apply` = take the whole list from each `todo/write` and clear to `null` on each `turn/start` (standing plan; `turn/end` keeps the finished checklist; every other event returns the same state reference), `view` = identity, `stateVersion` = 2. The key merges into `SessionProjectionMap` here (via the interface package's `/types` outlet); the framework drives the unit and carriers serve the value on the history tail page and the `session/projection` push frame. Compositions without the registry are unaffected. Lifetime rationale: [todo plan clears on next turn](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md). ## Export shape diff --git a/packages/todo/tool-todo/README.zh.md b/packages/todo/tool-todo/README.zh.md index c88dbf976f..9cea66147b 100644 --- a/packages/todo/tool-todo/README.zh.md +++ b/packages/todo/tool-todo/README.zh.md @@ -30,7 +30,7 @@ ## 会话投影 -当组合挂载了 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.md))时,本包在一个注入的子插件中注册 `todos` 投影单元:`init` = `null`(尚无写入)、`apply` = 从每个 `todo/write` 取整表,并在每个 `turn/start` 清为 `null`(当前有效计划;`turn/end` 保留刚完成的清单;其余事件都返回同一个状态引用)、`view` = 恒等、`stateVersion` = 2。该键在本包中合并进 `SessionProjectionMap`(经接口包的 `/types` 出口);框架驱动该单元,载体通过历史尾页与 `session/projection` 推送帧提供该值。未挂载注册表的组合不受影响。生命周期理由见 [在下一轮次清空 todo 计划](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md)。 +当组合挂载了 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md))时,本包在一个注入的子插件中注册 `todos` 投影单元:`init` = `null`(尚无写入)、`apply` = 从每个 `todo/write` 取整表,并在每个 `turn/start` 清为 `null`(当前有效计划;`turn/end` 保留刚完成的清单;其余事件都返回同一个状态引用)、`view` = 恒等、`stateVersion` = 2。该键在本包中合并进 `SessionProjectionMap`(经接口包的 `/types` 出口);框架驱动该单元,载体通过历史尾页与 `session/projection` 推送帧提供该值。未挂载注册表的组合不受影响。生命周期理由见 [在下一轮次清空 todo 计划](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md)。 ## 导出形状 diff --git a/packages/todo/tool-todo/tsconfig.json b/packages/todo/tool-todo/tsconfig.json index b35157e58d..6160fdbeb7 100644 --- a/packages/todo/tool-todo/tsconfig.json +++ b/packages/todo/tool-todo/tsconfig.json @@ -24,7 +24,7 @@ "path": "../../core/session" }, { - "path": "../../session-projection/session-projection" + "path": "../../session/session-projection" }, { "path": "../../support/invariants" diff --git a/packages/typert/generator/src/cordis-catalog.ts b/packages/typert/generator/src/cordis-catalog.ts index e5c2c15a00..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}](../core-data-structures/${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 [core-data-structures/](../core-data-structures/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 [core-data-structures/](../core-data-structures/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 d920ea82ca..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 = [ @@ -152,10 +151,11 @@ describe.skip('gen-cordis-catalog collectEvents', { timeout: 60_000 }, () => { it('accepts linked, foundation, generic-parameter, and explicitly exempt signature types', () => { const events = collectEvents(make( - ' /**\n * Carry linked and foundation types.\n * @param value - the linked value.\n * @param preset - deployment metadata outside the core catalog.\n * @param signal - cancellation.\n * @mode parallel\n */\n \'fix/typed\'(value: Readonly, preset: PresetSpec, signal: AbortSignal): Promise', + ' /**\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](../core-data-structures/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 6e6b93dfdc..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,10 +15,24 @@ 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/cordis/tool-cordis/src/api-catalog.ts'), + expected('packages/self-modification/tool-cordis/src/api-catalog.ts'), ) }) }) diff --git a/packages/typert/generator/tests/type-model.spec.ts b/packages/typert/generator/tests/type-model.spec.ts index 40a91e3ef5..f8c41fb157 100644 --- a/packages/typert/generator/tests/type-model.spec.ts +++ b/packages/typert/generator/tests/type-model.spec.ts @@ -419,7 +419,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => { it('rejects relative imports across face boundaries', () => { const root = copyFixture('typert-relative-face-') - const sourcePath = join(root, 'packages/client/src/index.ts') + const sourcePath = join(root, 'packages/client', 'src/index.ts') const source = readFileSync(sourcePath, 'utf8') .replace("from '@fixture/host'", "from '../../host/src/index.ts'") writeFileSync(sourcePath, source) @@ -435,7 +435,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => { join(root, 'packages/host/src/private.ts'), 'export interface PrivateHost { readonly value: string }\n', ) - const sourcePath = join(root, 'packages/client/src/index.ts') + const sourcePath = join(root, 'packages/client', 'src/index.ts') const source = readFileSync(sourcePath, 'utf8') .replace( "import type { HostAgent, Payload } from '@fixture/host'", @@ -458,7 +458,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => { join(root, 'packages/host/src/private.ts'), 'export interface PrivateHost { readonly value: string }\n', ) - const sourcePath = join(root, 'packages/client/src/index.ts') + const sourcePath = join(root, 'packages/client', 'src/index.ts') writeFileSync(sourcePath, [ readFileSync(sourcePath, 'utf8'), "export type { PrivateHost } from '@fixture/host/private'", @@ -472,7 +472,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => { it('rejects cross-face namespace re-exports until the model has a namespace target', () => { const root = copyFixture('typert-namespace-reexport-') - const sourcePath = join(root, 'packages/client/src/index.ts') + const sourcePath = join(root, 'packages/client', 'src/index.ts') writeFileSync(sourcePath, [ readFileSync(sourcePath, 'utf8'), "export type * as HostNamespace from '@fixture/host'", @@ -487,10 +487,10 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => { it('ignores cross-face namespace exports that are not package exports', () => { const root = copyFixture('typert-private-namespace-reexport-') writeFileSync( - join(root, 'packages/client/src/internal.ts'), + join(root, 'packages/client', 'src/internal.ts'), "export type * as HiddenHostNamespace from '@fixture/host'\n", ) - const sourcePath = join(root, 'packages/client/src/index.ts') + const sourcePath = join(root, 'packages/client', 'src/index.ts') writeFileSync(sourcePath, [ "import './internal.ts'", readFileSync(sourcePath, 'utf8'), @@ -503,7 +503,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => { it('records public symbols from explicit cross-face star re-exports', () => { const root = copyFixture('typert-star-reexport-') - const sourcePath = join(root, 'packages/client/src/index.ts') + const sourcePath = join(root, 'packages/client', 'src/index.ts') writeFileSync( sourcePath, readFileSync(sourcePath, 'utf8') @@ -1138,7 +1138,7 @@ describe('WorkspaceTypertGenerator', { timeout: 60_000 }, () => { it('rejects a public Typert subpath that points outside the root-level face artifact', () => { const root = copyFixture('typert-artifact-path-') - const manifestPath = join(root, 'packages/client/package.json') + const manifestPath = join(root, 'packages/client', 'package.json') const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { exports: Record } @@ -1154,7 +1154,7 @@ describe('WorkspaceTypertGenerator', { timeout: 60_000 }, () => { it('rejects absent Typert exports and package file entries', () => { const noSubpathRoot = copyFixture('typert-missing-artifact-export-') - const noSubpathManifest = join(noSubpathRoot, 'packages/client/package.json') + const noSubpathManifest = join(noSubpathRoot, 'packages/client', 'package.json') const noSubpath = JSON.parse(readFileSync(noSubpathManifest, 'utf8')) as Record noSubpath.exports = './lib/index.js' writeFileSync(noSubpathManifest, `${JSON.stringify(noSubpath, null, 2)}\n`) @@ -1163,7 +1163,7 @@ describe('WorkspaceTypertGenerator', { timeout: 60_000 }, () => { ) const invalidSubpathRoot = copyFixture('typert-invalid-artifact-export-') - const invalidSubpathManifest = join(invalidSubpathRoot, 'packages/client/package.json') + const invalidSubpathManifest = join(invalidSubpathRoot, 'packages/client', 'package.json') const invalidSubpath = JSON.parse(readFileSync(invalidSubpathManifest, 'utf8')) as { exports: Record } @@ -1174,7 +1174,7 @@ describe('WorkspaceTypertGenerator', { timeout: 60_000 }, () => { ) const noFilesRoot = copyFixture('typert-missing-artifact-files-') - const noFilesManifest = join(noFilesRoot, 'packages/client/package.json') + const noFilesManifest = join(noFilesRoot, 'packages/client', 'package.json') const noFiles = JSON.parse(readFileSync(noFilesManifest, 'utf8')) as Record delete noFiles.files writeFileSync(noFilesManifest, `${JSON.stringify(noFiles, null, 2)}\n`) @@ -1264,14 +1264,14 @@ function configureDualRuntimeClient(root: string, splitProjects: boolean): void const hostAggregate = JSON.parse(readFileSync(hostAggregatePath, 'utf8')) as { references: { path: string }[] } - hostAggregate.references.push({ path: './packages/client/tsconfig.host.json' }) + hostAggregate.references.push({ path: ['.', 'packages', 'client', 'tsconfig.host.json'].join('/') }) writeFileSync(hostAggregatePath, `${JSON.stringify(hostAggregate, null, 2)}\n`) const clientAggregatePath = join(root, 'tsconfig.client.json') const clientAggregate = JSON.parse(readFileSync(clientAggregatePath, 'utf8')) as { references: { path: string }[] } - clientAggregate.references = [{ path: './packages/client/tsconfig.client.json' }] + clientAggregate.references = [{ path: ['.', 'packages', 'client', 'tsconfig.client.json'].join('/') }] writeFileSync(clientAggregatePath, `${JSON.stringify(clientAggregate, null, 2)}\n`) } diff --git a/packages/ui/jsonrpc/README.i18n.yaml b/packages/ui/jsonrpc/README.i18n.yaml deleted file mode 100644 index d81f54f405..0000000000 --- a/packages/ui/jsonrpc/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 packages/ui/jsonrpc/README.md -README.md: 9cd4876b52f9527745b27041eb2555408c73fabd -README.zh.md: a2b979da448c04c0578382889be2ae3ea6076166 diff --git a/packages/ui/user-approval/README.i18n.yaml b/packages/ui/user-approval/README.i18n.yaml deleted file mode 100644 index cf9c8e1fce..0000000000 --- a/packages/ui/user-approval/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 packages/ui/user-approval/README.md -README.md: 7b87a75d1c7c43874c484bc11f8deed45cb523ce -README.zh.md: 3c02f8d673a62c3ee954da26f6341b433032a88c diff --git a/packages/util/environment/README.i18n.yaml b/packages/util/environment/README.i18n.yaml index 1c5f784bc4..633064ae4c 100644 --- a/packages/util/environment/README.i18n.yaml +++ b/packages/util/environment/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/util/environment/README.md -README.md: af6b0d9cc66b0bdfa1ad9ffb273260d0f4f06ddc -README.zh.md: 98c3c69ec96f835721e042960fe044fe075e6159 +README.md: 599a9ba747905b66452b57717dabcc6f4678a3dd +README.zh.md: 69f9c670bbb3ea1b2fbddaa7508a19dfb47fad12 diff --git a/packages/util/environment/README.md b/packages/util/environment/README.md index af6b0d9cc6..599a9ba747 100644 --- a/packages/util/environment/README.md +++ b/packages/util/environment/README.md @@ -32,5 +32,5 @@ const endpoint = environmentOf(ctx).get('DEEPSEEK_BASE_URL')?.value ## Known Limitations and Deferred Work -- **The snapshot is not a subprocess boundary** — every layer is also materialized into `process.env`, so ordinary project variables reach child processes under [`dsh-subprocess`](../../subprocess/subprocess/README.md)'s scrub. The product launcher's [`.env` contract](../../ui/app-boot/README.md#profiles) rejects bootstrap variables before materialization. +- **The snapshot is not a subprocess boundary** — every layer is also materialized into `process.env`, so ordinary project variables reach child processes under [`dsh-subprocess`](../../subprocess/subprocess/README.md)'s scrub. The product launcher's [`.env` contract](../../boot/app-boot/README.md#profiles) rejects bootstrap variables before materialization. - **No per-workspace layer** — the project layer is the *invoking* directory, fixed at launch. A workspace selected later in the Web UI contributes nothing, deliberately: following it would let a model's own workspace change the harness environment mid-session. diff --git a/packages/util/environment/README.zh.md b/packages/util/environment/README.zh.md index 98c3c69ec9..69f9c670bb 100644 --- a/packages/util/environment/README.zh.md +++ b/packages/util/environment/README.zh.md @@ -32,5 +32,5 @@ const endpoint = environmentOf(ctx).get('DEEPSEEK_BASE_URL')?.value ## Known Limitations and Deferred Work -- **快照不是子进程边界**:每一层同样会被物化进 `process.env`,因此项目里的普通变量会按 [`dsh-subprocess`](../../subprocess/subprocess/README.md) 的清洗规则抵达子进程。产品启动器的 [`.env` 契约](../../ui/app-boot/README.md#profiles) 会在物化之前拒绝 bootstrap 变量。 +- **快照不是子进程边界**:每一层同样会被物化进 `process.env`,因此项目里的普通变量会按 [`dsh-subprocess`](../../subprocess/subprocess/README.md) 的清洗规则抵达子进程。产品启动器的 [`.env` 契约](../../boot/app-boot/README.md#profiles) 会在物化之前拒绝 bootstrap 变量。 - **没有按工作区划分的层**:项目层是*调用*目录,在启动时固定。之后在 Web UI 中选择的工作区不贡献任何内容,这是刻意的:跟随它等于让模型自己的工作区在会话中途改变 harness 的环境。 diff --git a/packages/web/README.i18n.yaml b/packages/web/README.i18n.yaml index cd412d1a99..87177273da 100644 --- a/packages/web/README.i18n.yaml +++ b/packages/web/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/web/README.md -README.md: 65b3dde1e2f9c35c308a10ca80245064f7a0361e -README.zh.md: 43b8b7d9c53336e984d9c61fa9b612fc1733e31f +README.md: 811c8b3439a8962116df0e8b3ab4f6e282ecb3d2 +README.zh.md: e328447d5d026c0abc815b269179ba6c4cd9acfc diff --git a/packages/web/README.md b/packages/web/README.md index 65b3dde1e2..811c8b3439 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -14,3 +14,5 @@ This family provides provider-neutral web search and fetch operations plus the m | [`tool-web/`](tool-web/README.md) | Exposes web search and fetch to the model | registers on `ctx.tools` | The [web capability decision](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) records why search and fetch share one provider-selection service. + +The subsystem reference — search/fetch requests and results, availability, `WebError` — is [docs/subsystems/web.md](../../docs/subsystems/web.md); rationale (including deferred SSRF protection) in the [web capability seam Agent Note](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md). diff --git a/packages/web/README.zh.md b/packages/web/README.zh.md index 43b8b7d9c5..e328447d5d 100644 --- a/packages/web/README.zh.md +++ b/packages/web/README.zh.md @@ -14,3 +14,5 @@ | [`tool-web/`](tool-web/README.md) | 向模型公开 web 搜索和抓取 | 注册到 `ctx.tools` | [web 能力决策](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)记录了搜索和抓取共用一项提供方选择服务的原因。 + +子系统参考——搜索/抓取请求与结果、可用性、`WebError`——见 [docs/subsystems/web.md](../../docs/subsystems/web.md);依据(含延后的 SSRF 防护)见 [web 能力 seam Agent Note](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)。 diff --git a/packages/web/tool-web/README.i18n.yaml b/packages/web/tool-web/README.i18n.yaml index eb57a7b20a..4751678c04 100644 --- a/packages/web/tool-web/README.i18n.yaml +++ b/packages/web/tool-web/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/web/tool-web/README.md -README.md: 791ea87c655444e639ef85ccce737066ead8b749 -README.zh.md: ffcf2d9813dcb94d8106b2c7d5f8ee9fc25e1aaa +README.md: 646601bb5bd6c27876918b7aca49962ae34bae33 +README.zh.md: 192b7020f5fac026f325448630b2e17d7ce6a0c4 diff --git a/packages/web/tool-web/README.md b/packages/web/tool-web/README.md index 791ea87c65..646601bb5b 100644 --- a/packages/web/tool-web/README.md +++ b/packages/web/tool-web/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The model-facing web tool suite — `web_search` and `web_fetch` — over the [web capability seam](../web/README.md) (`ctx.web`). It owns model-facing concerns only: tool names, JSON schemas, snake_case argument names, prompt sections, the result-count bound, result formatting, HTML→markdown presentation, and the UI presentation projection — `presentCall`, `presentResult` (a `card: 'web'` result card discriminated by `kind: 'search' | 'fetch'`), and the `output.presentationMeta` that carries the structured search sources or the fetch summary the lossy render text cannot (see the [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.md)). All web access goes through `ctx.web`; this package never imports a concrete provider. Neither tool exposes a model-facing timeout — each tool's cooperative tool-call budget is declared here via config (`fetchTimeoutMs`/`searchTimeoutMs`, attached as `ToolDefinition.timeoutMs`) and enforced by [`@deepseek-ai/dsh-timeout-policy`](../../timeout/timeout-policy/README.md) (a `tools/execute` wrapper); each tool just forwards `exec.signal` to the seam. +The model-facing web tool suite — `web_search` and `web_fetch` — over the [web capability seam](../web/README.md) (`ctx.web`). It owns model-facing concerns only: tool names, JSON schemas, snake_case argument names, prompt sections, the result-count bound, result formatting, HTML→markdown presentation, and the UI presentation projection — `presentCall`, `presentResult` (a `card: 'web'` result card discriminated by `kind: 'search' | 'fetch'`), and the `output.presentationMeta` that carries the structured search sources or the fetch summary the lossy render text cannot (see the [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.md)). All web access goes through `ctx.web`; this package never imports a concrete provider. Neither tool exposes a model-facing timeout — each tool's cooperative tool-call budget is declared here via config (`fetchTimeoutMs`/`searchTimeoutMs`, attached as `ToolDefinition.timeoutMs`) and enforced by [`@deepseek-ai/dsh-timeout-policy`](../../guard/timeout-policy/README.md) (a `tools/execute` wrapper); each tool just forwards `exec.signal` to the seam. Each tool is registered independently; a product that wants only one disables the other via config (`{ search: false }` / `{ fetch: false }`). Search guidance mentions `web_fetch` only when fetch is also config-enabled; a search-only composition instead tells the model to use returned snippets and cite their URLs. @@ -28,7 +28,7 @@ The normalized seam results are also the canonical tool values: `WebSearchResult | `searchTimeoutMs` | `30000` | Cooperative tool-call timeout budget (ms) for `web_search`. | | `fetchMaxOutputChars` | `200000` | Cap on source characters converted synchronously and on one complete `web_fetch` output (header, rendered body, and footer); a cut body gets the truncation notice when it fits. | -`fetchTimeoutMs`/`searchTimeoutMs` declare each tool's cooperative timeout budget (attached as `ToolDefinition.timeoutMs`), enforced by [`@deepseek-ai/dsh-timeout-policy`](../../timeout/timeout-policy/README.md); the model-facing schema exposes no timeout argument. `fetchMaxOutputChars` bounds both synchronous conversion work and the complete rendered result: only that many source characters are converted, and the header, converted prefix, and truncation notice are then capped together. The default leaves headroom above the local provider's 100,000-character body cap, but rendered expansion can still make the final bound truncate the result. +`fetchTimeoutMs`/`searchTimeoutMs` declare each tool's cooperative timeout budget (attached as `ToolDefinition.timeoutMs`), enforced by [`@deepseek-ai/dsh-timeout-policy`](../../guard/timeout-policy/README.md); the model-facing schema exposes no timeout argument. `fetchMaxOutputChars` bounds both synchronous conversion work and the complete rendered result: only that many source characters are converted, and the header, converted prefix, and truncation notice are then capped together. The default leaves headroom above the local provider's 100,000-character body cap, but rendered expansion can still make the final bound truncate the result. ```yaml - id: tool-web diff --git a/packages/web/tool-web/README.zh.md b/packages/web/tool-web/README.zh.md index ffcf2d9813..192b7020f5 100644 --- a/packages/web/tool-web/README.zh.md +++ b/packages/web/tool-web/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -面向模型的 web 工具套件 `web_search` 与 `web_fetch`,构建于 [web 能力 seam](../web/README.md)(`ctx.web`)之上。它只负责面向模型的事项:工具名称、JSON Schema、snake_case 参数名称、提示词区段、结果数量上限、结果格式、HTML→markdown 呈现,以及 UI 呈现投影——`presentCall`、`presentResult`(以 `kind: 'search' | 'fetch'` 区分的 `card: 'web'` 结果卡片),以及承载有损渲染文本无法携带的结构化搜索来源或抓取摘要的 `output.presentationMeta`(见 [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.md))。所有 web 访问都通过 `ctx.web`;该包绝不导入具体提供方。两个工具都不公开面向模型的超时:每个工具的协作式工具调用超时预算通过配置在此声明(`fetchTimeoutMs`/`searchTimeoutMs`,附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-timeout-policy`](../../timeout/timeout-policy/README.md)(`tools/execute` 包装层)强制执行;每个工具只把 `exec.signal` 转发给 seam。 +面向模型的 web 工具套件 `web_search` 与 `web_fetch`,构建于 [web 能力 seam](../web/README.md)(`ctx.web`)之上。它只负责面向模型的事项:工具名称、JSON Schema、snake_case 参数名称、提示词区段、结果数量上限、结果格式、HTML→markdown 呈现,以及 UI 呈现投影——`presentCall`、`presentResult`(以 `kind: 'search' | 'fetch'` 区分的 `card: 'web'` 结果卡片),以及承载有损渲染文本无法携带的结构化搜索来源或抓取摘要的 `output.presentationMeta`(见 [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.md))。所有 web 访问都通过 `ctx.web`;该包绝不导入具体提供方。两个工具都不公开面向模型的超时:每个工具的协作式工具调用超时预算通过配置在此声明(`fetchTimeoutMs`/`searchTimeoutMs`,附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-timeout-policy`](../../guard/timeout-policy/README.md)(`tools/execute` 包装层)强制执行;每个工具只把 `exec.signal` 转发给 seam。 每个工具独立注册;只需要其中一个工具的产品可以通过配置禁用另一个(`{ search: false }`/`{ fetch: false }`)。仅当抓取也通过配置启用时,搜索指引才会提及 `web_fetch`;仅启用搜索的组合则会要求模型使用返回的 snippet 并引用其 URL。 @@ -28,7 +28,7 @@ | `searchTimeoutMs` | `30000` | `web_search` 的协作式工具调用超时预算(ms)。 | | `fetchMaxOutputChars` | `200000` | 同步转换的源字符数与单次完整 `web_fetch` 输出的上限(状态头、渲染后的主体与页脚合并计算);主体被截断时,在能容纳的情况下附带截断提示。 | -`fetchTimeoutMs`/`searchTimeoutMs` 声明每个工具的协作式超时预算(附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-timeout-policy`](../../timeout/timeout-policy/README.md) 强制执行;面向模型的 schema 不公开超时参数。`fetchMaxOutputChars` 同时限制同步转换工作量和完整渲染结果:只转换至多该数量的源字符,随后对状态头、转换后的前缀和截断提示合并设限。默认值为本地提供方的 100,000 字符主体上限留出余量,但渲染膨胀仍可能使最终上限截断结果。 +`fetchTimeoutMs`/`searchTimeoutMs` 声明每个工具的协作式超时预算(附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-timeout-policy`](../../guard/timeout-policy/README.md) 强制执行;面向模型的 schema 不公开超时参数。`fetchMaxOutputChars` 同时限制同步转换工作量和完整渲染结果:只转换至多该数量的源字符,随后对状态头、转换后的前缀和截断提示合并设限。默认值为本地提供方的 100,000 字符主体上限留出余量,但渲染膨胀仍可能使最终上限截断结果。 ```yaml - id: tool-web diff --git a/packages/web/tool-web/tsconfig.json b/packages/web/tool-web/tsconfig.json index f6684272ce..232d6f6c50 100644 --- a/packages/web/tool-web/tsconfig.json +++ b/packages/web/tool-web/tsconfig.json @@ -25,7 +25,7 @@ "path": "../../core/system-prompt" }, { - "path": "../../timeout/timeout-policy" + "path": "../../guard/timeout-policy" }, { "path": "../web" diff --git a/packages/web/web-fetch-local/README.i18n.yaml b/packages/web/web-fetch-local/README.i18n.yaml index 0e487b6ef5..5511f72975 100644 --- a/packages/web/web-fetch-local/README.i18n.yaml +++ b/packages/web/web-fetch-local/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/web/web-fetch-local/README.md -README.md: 8cadba2de7a2708252ebc7143840825fd4fe4549 -README.zh.md: b3f0707c448da7abaaafe2cdcf2520914ffff932 +README.md: 531bfc94a4b2867b1a81d2e0b398ec403186b347 +README.zh.md: 260227679b72ec993dbb9e42a9d2a814bf691e2c diff --git a/packages/web/web-fetch-local/README.md b/packages/web/web-fetch-local/README.md index 8cadba2de7..531bfc94a4 100644 --- a/packages/web/web-fetch-local/README.md +++ b/packages/web/web-fetch-local/README.md @@ -10,7 +10,7 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i The provider owns **safe resource retrieval**: URL validation, HTTP transport, redirect policy, a resource-backstop timeout, abort propagation, byte caps, charset decoding, content-type classification, and binary rejection. `@deepseek-ai/dsh-tool-web` owns **presentation** (HTML→markdown, truncation formatting). A non-2xx HTTP response is a *result* (status code + decoded body), not an error; `WebError` is reserved for failures to safely retrieve or represent the resource. -The provider's `timeoutMs` is a resource backstop for direct `ctx.web.fetch()` callers and misconfigured deployments, not the model-facing tool-call budget. [`dsh-timeout-policy`](../../timeout/timeout-policy/README.md) owns the `web_fetch` tool-call budget by arming `exec.signal`. +The provider's `timeoutMs` is a resource backstop for direct `ctx.web.fetch()` callers and misconfigured deployments, not the model-facing tool-call budget. [`dsh-timeout-policy`](../../guard/timeout-policy/README.md) owns the `web_fetch` tool-call budget by arming `exec.signal`. A shipping web-tool deployment sets the provider backstop above the tool budget, so model calls normally return `TOOL_TIMEOUT`. If the outer deadline reaches the provider first, the provider reports `WEB_ABORTED` and the outer policy replaces it with `TOOL_TIMEOUT`. `WEB_FETCH_TIMEOUT` therefore identifies a direct seam caller whose provider budget elapsed. diff --git a/packages/web/web-fetch-local/README.zh.md b/packages/web/web-fetch-local/README.zh.md index b3f0707c44..260227679b 100644 --- a/packages/web/web-fetch-local/README.zh.md +++ b/packages/web/web-fetch-local/README.zh.md @@ -10,7 +10,7 @@ 提供方拥有**安全资源获取**:URL 验证、HTTP 传输、重定向策略、资源兜底超时、中止传播、字节上限、charset 解码、内容类型分类与二进制拒绝。`@deepseek-ai/dsh-tool-web` 拥有**呈现**(HTML→markdown、截断格式)。非 2xx HTTP 响应是*结果*(状态码 + 解码主体),不是错误;`WebError` 只用于无法安全获取或表示资源的失败。 -提供方的 `timeoutMs` 是直接 `ctx.web.fetch()` 调用方和配置有误的部署所用的资源兜底,不是面向模型的工具调用预算。[`dsh-timeout-policy`](../../timeout/timeout-policy/README.md) 拥有 `web_fetch` 工具调用预算,并让 `exec.signal` 在超时时触发,以强制执行该预算。 +提供方的 `timeoutMs` 是直接 `ctx.web.fetch()` 调用方和配置有误的部署所用的资源兜底,不是面向模型的工具调用预算。[`dsh-timeout-policy`](../../guard/timeout-policy/README.md) 拥有 `web_fetch` 工具调用预算,并让 `exec.signal` 在超时时触发,以强制执行该预算。 已交付的 web 工具部署会把提供方兜底设为高于工具预算,因此模型调用通常返回 `TOOL_TIMEOUT`。如果外层截止期限先于提供方的兜底超时触发,提供方会报告 `WEB_ABORTED`,外层策略再将其替换为 `TOOL_TIMEOUT`。因此,`WEB_FETCH_TIMEOUT` 表明直接 seam 调用方的提供方预算已经耗尽。 diff --git a/packages/workflow/README.i18n.yaml b/packages/workflow/README.i18n.yaml index f02915a31d..05ad673c61 100644 --- a/packages/workflow/README.i18n.yaml +++ b/packages/workflow/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/workflow/README.md -README.md: 2416e26e73340a6d8624b8e77681dfc9f55fe7c5 -README.zh.md: a5c94a0a9799d78e5cca72394385b38d2fb2ba70 +README.md: 7b19cfd37f9b6870825d5e0761aed92908206b3a +README.zh.md: 33b3f26780da1bf4efcd78242b2cfb212cef4c22 diff --git a/packages/workflow/README.md b/packages/workflow/README.md index 2416e26e73..7b19cfd37f 100644 --- a/packages/workflow/README.md +++ b/packages/workflow/README.md @@ -12,3 +12,5 @@ This family runs model-authored orchestration workflows over subagents and expos | [`tool-ralph/`](tool-ralph/README.md) | Exposes the fixed fresh-agent Ralph workflow | registers on `ctx.tools` | Worker threads isolate workflow execution from the host event loop but are not a security boundary. See the [dynamic-workflow](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md) and [Ralph tool](../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md) decisions. + +The subsystem reference — start requests, `WorkflowMeta`, results, live runs, `workflow/*` events — is [docs/subsystems/workflow.md](../../docs/subsystems/workflow.md); decisions in the [dynamic-workflows](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md) and [Ralph consumer](../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md) Agent Notes. diff --git a/packages/workflow/README.zh.md b/packages/workflow/README.zh.md index a5c94a0a97..33b3f26780 100644 --- a/packages/workflow/README.zh.md +++ b/packages/workflow/README.zh.md @@ -12,3 +12,5 @@ | [`tool-ralph/`](tool-ralph/README.md) | 公开使用全新 agent(智能体)的固定 Ralph 工作流 | 注册到 `ctx.tools` | worker thread 将工作流执行与宿主事件循环隔离,但不构成安全边界。参见[动态工作流](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md)和 [Ralph 工具](../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md)决策。 + +子系统参考——启动请求、`WorkflowMeta`、结果、实时运行、`workflow/*` 事件——见 [docs/subsystems/workflow.md](../../docs/subsystems/workflow.md);决策见[动态工作流](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md)与 [Ralph 消费方](../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md) Agent Note。 diff --git a/packages/workspace/README.i18n.yaml b/packages/workspace/README.i18n.yaml index 93dea4b3f0..c7723f6c56 100644 --- a/packages/workspace/README.i18n.yaml +++ b/packages/workspace/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/workspace/README.md -README.md: 9f6ead776c1e5d36af5b6917ad8f27725b11e6a0 -README.zh.md: aa691ec681112f4c25a6391c33eabd2711c7abfc +README.md: 7e30f26a9854031b4907ad3e7edb35da5986d5d9 +README.zh.md: 6315ccb6b3a8b86161728e8296746efd0c334a29 diff --git a/packages/workspace/README.md b/packages/workspace/README.md index 9f6ead776c..7e30f26a98 100644 --- a/packages/workspace/README.md +++ b/packages/workspace/README.md @@ -9,3 +9,5 @@ This family owns persistent workspaces: user directories with titles and ordered | [`workspace/`](workspace/README.md) | Registers workspaces and accounts for their sessions | `ctx.workspace` | The [workspace package reference](workspace/README.md) owns lifecycle, persistence, and deletion semantics. + +The subsystem reference — the entity, realpath canon, registration/resolution — is [docs/subsystems/workspace.md](../../docs/subsystems/workspace.md); storage design in the [domain KV storage Agent Note](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md). diff --git a/packages/workspace/README.zh.md b/packages/workspace/README.zh.md index aa691ec681..6315ccb6b3 100644 --- a/packages/workspace/README.zh.md +++ b/packages/workspace/README.zh.md @@ -9,3 +9,5 @@ | [`workspace/`](workspace/README.md) | 注册 workspace 并记录其会话归属 | `ctx.workspace` | [workspace 包参考](workspace/README.md)负责生命周期、持久化和删除语义。 + +子系统参考——实体、realpath 规范、注册/解析——见 [docs/subsystems/workspace.md](../../docs/subsystems/workspace.md);存储设计见 [domain KV 存储 Agent Note](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md)。 diff --git a/packages/workspace/workspace/README.i18n.yaml b/packages/workspace/workspace/README.i18n.yaml index aa3db1bef6..63d5ce0e8e 100644 --- a/packages/workspace/workspace/README.i18n.yaml +++ b/packages/workspace/workspace/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/workspace/workspace/README.md -README.md: bfa044de50fc71dca95637487df50dc551d40e1b -README.zh.md: f3be9611eba45db3719ce76ad6625f4787fab9ac +README.md: 057765e38de9cc700210eb8edeb1ddc7ffc861ff +README.zh.md: 7416875dbf2ee1652f6e1fa1663144d7407a1ae7 diff --git a/packages/workspace/workspace/README.md b/packages/workspace/workspace/README.md index bfa044de50..057765e38d 100644 --- a/packages/workspace/workspace/README.md +++ b/packages/workspace/workspace/README.md @@ -12,6 +12,7 @@ The entity/storage rationale lives in the [domain Agent Note](../../../.agents/n - `ctx.workspace.get(id)` / `list()` / `resolveByPath(path)` — cache-served lookups. `list()` is synchronous and follows durable registry order; `resolveByPath` is async because it applies the same `realpath` canon and rejects a missing path rather than creating it. - `ctx.workspace.delete(id)` — removes only the Workspace registration, its durable order entry, and its session account. Unknown ids return `false`; a removed record returns `true`. The directory, user files, live Sessions, and persisted session logs are never touched, so those Sessions become Ungrouped. A table-write failure restores the prior order and published entity. - `Workspace.attachSession(id)` — validates a live or persisted session header cwd against the workspace path and prepends a new id. Unknown sessions, absent/unresolvable/non-directory cwd values, and mismatches reject without writing. `detachSession` removes only the candidate index entry. +- `Workspace.insertSessionBefore(id, before?)` — moves an accounted session within the manual order, DOM-insertBefore-like: before the anchor, or appended when the anchor is omitted. A session or anchor absent from the account rejects without writing; a move to the current position resolves without writing. Workspace order never changes. - `ctx.workspace.archiveSession(id)` / `archivedSessionIds` — the registry-global archive set, layered over workspace accounting: an archived session disappears from grouping surfaces but keeps its session log and its `sessionIds` slot, so a future unarchive restores its position. Archiving accepts any live or persisted session (accounted or Ungrouped), resolves without writing for an already archived id, and rejects an unknown id. State written before the field existed parses with an empty set. - `Workspace.sessionIds` — synchronous id-plus-canonical-cwd membership projection in durable candidate order. Missing headers, invalid cwd values, and mismatches are filtered; the next workspace mutation prunes them. A medium indexing one session under two workspaces, claiming one path from two records, or diverging from durable workspace order rejects at startup. - `Workspace.status()` — uncached directory check, `'ok' | 'missing-dir'`; a missing directory never mutates the record. diff --git a/packages/workspace/workspace/README.zh.md b/packages/workspace/workspace/README.zh.md index f3be9611eb..7416875dbf 100644 --- a/packages/workspace/workspace/README.zh.md +++ b/packages/workspace/workspace/README.zh.md @@ -12,6 +12,7 @@ DeepSeek Harness 的 Workspace 实体注册表(`ctx.workspace`):通过领 - `ctx.workspace.get(id)`/`list()`/`resolveByPath(path)`:由缓存提供的查找。`list()` 为同步操作,并遵循持久注册表顺序;`resolveByPath` 为异步操作,因为它采用相同的 `realpath` 规范化方式,并会拒绝缺失路径,而不是创建路径。 - `ctx.workspace.delete(id)`:只移除 Workspace 注册记录、对应的持久顺序条目及会话归属记录。未知 id 返回 `false`,成功移除记录则返回 `true`。目录、用户文件、活跃会话和持久化会话日志绝不受影响,因此相关会话会进入 Ungrouped。表写入失败时会恢复原顺序和此前发布的实体。 - `Workspace.attachSession(id)`:对照 workspace 路径验证实时或已持久化的会话头 cwd,并将新 id 前置。未知会话、缺失/无法解析/非目录的 cwd 值和不匹配情况都会在不写入的前提下被拒绝。`detachSession` 只移除候选索引条目。 +- `Workspace.insertSessionBefore(id, before?)`:在手动顺序内移动一个已记账的会话,语义类似 DOM 的 insertBefore:插到锚点之前,省略锚点则追加到末尾。会话或锚点不在记账中时拒绝且不写入;移动到当前位置时直接完成且不写入。Workspace 顺序绝不改变。 - `ctx.workspace.archiveSession(id)`/`archivedSessionIds`:覆盖在 workspace 记账之上的注册表级全局归档集合:被归档的会话从各分组视图中消失,但其会话日志和 `sessionIds` 席位保持不变,未来取消归档时可恢复原位置。归档接受任何实时或已持久化的会话(无论已记账还是 Ungrouped),对已归档的 id 直接完成而不写入,并拒绝未知 id。在该字段出现之前写入的状态解析为一个空集合。 - `Workspace.sessionIds`:按持久候选顺序提供同步 id 加规范 cwd 成员投影。缺失头部、无效 cwd 值和不匹配情况都被过滤;下一次 workspace 变更会剪除它们。如果同一存储介质将一个会话索引到两个 workspace 下、用两条记录声明同一路径,或偏离持久 workspace 顺序,启动会被拒绝。 - `Workspace.status()`:未缓存的目录检查,返回 `'ok' | 'missing-dir'`;目录缺失绝不会改动记录。 diff --git a/packages/workspace/workspace/src/types.ts b/packages/workspace/workspace/src/types.ts index 09d37213cc..555e94d45d 100644 --- a/packages/workspace/workspace/src/types.ts +++ b/packages/workspace/workspace/src/types.ts @@ -59,7 +59,9 @@ export interface Workspace { /** * Prepend a session to this workspace's candidate account. An already - * accounted id resolves without writing. A new id's live or persisted + * accounted id resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs. A new id's + * live or persisted * header cwd must resolve to an existing directory equal to {@link path}; * unknown ids, missing or invalid cwd values, and mismatches reject without * writing. @@ -73,7 +75,9 @@ export interface Workspace { * with an anchor the session lands before it, without one it appends to the * end. Only the moved id changes position. A session or anchor absent from * the account rejects without writing; a move to the current position - * resolves without writing (decided on the domain write chain). + * resolves without writing, aside from the durable filtered-candidate + * prune every accepted mutation performs; decided on the domain write + * chain. * @param sessionId - The accounted session to move. * @param beforeSessionId - Accounted anchor to insert before; omitted appends. * @returns resolution after durability. @@ -82,8 +86,9 @@ export interface Workspace { /** * Remove a session from this workspace's account. Idempotent: an id not on - * the account resolves without writing (decided on the domain write chain, - * like attach). Never touches the session's own stored log. + * the account resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs; decided on + * the domain write chain like attach. Never touches the session's own stored log. * @param sessionId - The session to remove. * @returns resolution after durability. */ diff --git a/packages/workspace/workspace/tsconfig.json b/packages/workspace/workspace/tsconfig.json index 0bddf4672c..bbdf80fae2 100644 --- a/packages/workspace/workspace/tsconfig.json +++ b/packages/workspace/workspace/tsconfig.json @@ -27,7 +27,7 @@ "path": "../../core/session" }, { - "path": "../../session-persistence/session-persistence" + "path": "../../session/session-persistence" }, { "path": "../../util/brand" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7118f0f259..68de6afa7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,7 +142,7 @@ importers: version: link:../../vendor/timer '@deepseek-ai/dsh-app-boot': specifier: workspace:^ - version: link:../../packages/ui/app-boot + version: link:../../packages/boot/app-boot '@deepseek-ai/dsh-base': specifier: workspace:^ version: link:../../packages/bundle/base @@ -208,7 +208,7 @@ importers: version: link:../../packages/llm/token-meter '@deepseek-ai/dsh-tool-ask-user': specifier: workspace:^ - version: link:../../packages/ui/tool-ask-user + version: link:../../packages/interaction/tool-ask-user '@deepseek-ai/dsh-tool-bash': specifier: workspace:^ version: link:../../packages/bash/tool-bash @@ -217,7 +217,7 @@ importers: version: link:../../packages/pty/tool-bash-persistent '@deepseek-ai/dsh-tool-cordis': specifier: workspace:^ - version: link:../../packages/cordis/tool-cordis + version: link:../../packages/self-modification/tool-cordis '@deepseek-ai/dsh-tool-fs': specifier: workspace:^ version: link:../../packages/fs/tool-fs @@ -399,7 +399,7 @@ importers: version: link:../packages/examples/agent-spine-demo '@deepseek-ai/dsh-app-boot': specifier: workspace:* - version: link:../packages/ui/app-boot + version: link:../packages/boot/app-boot '@deepseek-ai/dsh-bash': specifier: workspace:* version: link:../packages/bash/bash @@ -423,7 +423,7 @@ importers: version: link:../packages/goal/command-goal '@deepseek-ai/dsh-commands': specifier: workspace:* - version: link:../packages/ui/commands + version: link:../packages/interaction/commands '@deepseek-ai/dsh-compact': specifier: workspace:* version: link:../packages/compact/compact @@ -468,7 +468,7 @@ importers: version: link:../packages/support/invariants '@deepseek-ai/dsh-jsonrpc': specifier: workspace:* - version: link:../packages/ui/jsonrpc + version: link:../packages/scaffold/server '@deepseek-ai/dsh-llm': specifier: workspace:* version: link:../packages/llm/llm @@ -492,7 +492,7 @@ importers: version: link:../packages/lsp/lsp-local '@deepseek-ai/dsh-permission': specifier: workspace:* - version: link:../packages/ui/permission + version: link:../packages/interaction/permission '@deepseek-ai/dsh-plan-mode': specifier: workspace:* version: link:../packages/plan/plan-mode @@ -510,7 +510,7 @@ importers: version: link:../packages/guard/repeat-tool-guard '@deepseek-ai/dsh-repository-plugin': specifier: workspace:* - version: link:../packages/cordis/repository-plugin + version: link:../packages/self-modification/repository-plugin '@deepseek-ai/dsh-sandbox': specifier: workspace:* version: link:../packages/sandbox/sandbox @@ -528,13 +528,13 @@ importers: version: link:../packages/core/session '@deepseek-ai/dsh-session-checkpoint-policy': specifier: workspace:* - version: link:../packages/session-persistence/session-checkpoint-policy + version: link:../packages/session/session-checkpoint-policy '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:* - version: link:../packages/session-persistence/session-persistence-jsonl + version: link:../packages/session/session-persistence-jsonl '@deepseek-ai/dsh-session-projection': specifier: workspace:* - version: link:../packages/session-projection/session-projection + version: link:../packages/session/session-projection '@deepseek-ai/dsh-session-query': specifier: workspace:* version: link:../packages/session-query/session-query @@ -546,13 +546,13 @@ importers: version: link:../packages/context/session-reference '@deepseek-ai/dsh-session-telemetry-otel': specifier: workspace:* - version: link:../packages/telemetry/session-telemetry-otel + version: link:../packages/session/session-telemetry-otel '@deepseek-ai/dsh-session-title': specifier: workspace:* - version: link:../packages/session-title/session-title + version: link:../packages/session/session-title '@deepseek-ai/dsh-session-title-first-message-llm': specifier: workspace:* - version: link:../packages/session-title/session-title-first-message-llm + version: link:../packages/session/session-title-first-message-llm '@deepseek-ai/dsh-settings-local': specifier: workspace:* version: link:../packages/settings/settings-local @@ -606,13 +606,13 @@ importers: version: link:../packages/context/time-context '@deepseek-ai/dsh-timeout-policy': specifier: workspace:* - version: link:../packages/timeout/timeout-policy + version: link:../packages/guard/timeout-policy '@deepseek-ai/dsh-token-meter': specifier: workspace:* version: link:../packages/llm/token-meter '@deepseek-ai/dsh-tool-ask-user': specifier: workspace:* - version: link:../packages/ui/tool-ask-user + version: link:../packages/interaction/tool-ask-user '@deepseek-ai/dsh-tool-bash': specifier: workspace:* version: link:../packages/bash/tool-bash @@ -621,7 +621,7 @@ importers: version: link:../packages/pty/tool-bash-persistent '@deepseek-ai/dsh-tool-cordis': specifier: workspace:* - version: link:../packages/cordis/tool-cordis + version: link:../packages/self-modification/tool-cordis '@deepseek-ai/dsh-tool-fs': specifier: workspace:* version: link:../packages/fs/tool-fs @@ -678,10 +678,10 @@ importers: version: link:../packages/core/tools '@deepseek-ai/dsh-user-approval': specifier: workspace:* - version: link:../packages/ui/user-approval + version: link:../packages/interaction/user-approval '@deepseek-ai/dsh-user-interaction': specifier: workspace:* - version: link:../packages/ui/user-interaction + version: link:../packages/interaction/user-interaction '@deepseek-ai/dsh-web': specifier: workspace:* version: link:../packages/web/web @@ -755,7 +755,7 @@ importers: version: link:../../core/tools '@deepseek-ai/dsh-user-approval': specifier: workspace:^ - version: link:../../ui/user-approval + version: link:../../interaction/user-approval cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -805,7 +805,7 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-typert-registry': specifier: workspace:^ version: link:../../typert/registry @@ -851,7 +851,7 @@ importers: version: link:../../util/paths '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools @@ -980,7 +980,7 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-subprocess-local': specifier: workspace:^ version: link:../../subprocess/subprocess-local @@ -1001,7 +1001,7 @@ importers: version: link:../../core/tools '@deepseek-ai/dsh-user-approval': specifier: workspace:^ - version: link:../../ui/user-approval + version: link:../../interaction/user-approval cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -1055,6 +1055,46 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis + packages/boot/app-boot: + dependencies: + js-yaml: + specifier: ^4.2.0 + version: 4.2.0 + devDependencies: + '@cordisjs/plugin-group': + specifier: workspace:^ + version: link:../../../vendor/group + '@cordisjs/plugin-hmr': + specifier: workspace:^ + version: link:../../../vendor/hmr + '@cordisjs/plugin-include': + specifier: workspace:^ + version: link:../../../vendor/include + '@cordisjs/plugin-loader': + specifier: workspace:^ + version: link:../../../vendor/loader + '@cordisjs/plugin-timer': + specifier: workspace:^ + version: link:../../../vendor/timer + '@deepseek-ai/dsh-environment': + specifier: workspace:^ + version: link:../../util/environment + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-paths': + specifier: workspace:^ + version: link:../../util/paths + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + packages/bundle/base: dependencies: '@cordisjs/plugin-hmr': @@ -1089,7 +1129,7 @@ importers: version: link:../../goal/command-goal '@deepseek-ai/dsh-commands': specifier: workspace:^ - version: link:../../ui/commands + version: link:../../interaction/commands '@deepseek-ai/dsh-compact-basic': specifier: workspace:^ version: link:../../compact/compact-basic @@ -1125,7 +1165,7 @@ importers: version: link:../../llm/llm-retry '@deepseek-ai/dsh-permission': specifier: workspace:^ - version: link:../../ui/permission + version: link:../../interaction/permission '@deepseek-ai/dsh-plan-mode': specifier: workspace:^ version: link:../../plan/plan-mode @@ -1134,7 +1174,7 @@ importers: version: link:../../guard/repeat-tool-guard '@deepseek-ai/dsh-repository-plugin': specifier: workspace:^ - version: link:../../cordis/repository-plugin + version: link:../../self-modification/repository-plugin '@deepseek-ai/dsh-sandbox-local': specifier: workspace:^ version: link:../../sandbox/sandbox-local @@ -1146,25 +1186,25 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-checkpoint-policy': specifier: workspace:^ - version: link:../../session-persistence/session-checkpoint-policy + version: link:../../session/session-checkpoint-policy '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection '@deepseek-ai/dsh-session-query-sqlite': specifier: workspace:^ version: link:../../session-query/session-query-sqlite '@deepseek-ai/dsh-session-telemetry-otel': specifier: workspace:^ - version: link:../../telemetry/session-telemetry-otel + version: link:../../session/session-telemetry-otel '@deepseek-ai/dsh-session-title': specifier: workspace:^ - version: link:../../session-title/session-title + version: link:../../session/session-title '@deepseek-ai/dsh-session-title-first-message-llm': specifier: workspace:^ - version: link:../../session-title/session-title-first-message-llm + version: link:../../session/session-title-first-message-llm '@deepseek-ai/dsh-settings-local': specifier: workspace:^ version: link:../../settings/settings-local @@ -1203,7 +1243,7 @@ importers: version: link:../../tasks/tasks-local '@deepseek-ai/dsh-timeout-policy': specifier: workspace:^ - version: link:../../timeout/timeout-policy + version: link:../../guard/timeout-policy '@deepseek-ai/dsh-token-meter': specifier: workspace:^ version: link:../../llm/token-meter @@ -1260,10 +1300,10 @@ importers: version: link:../../typert/registry '@deepseek-ai/dsh-user-approval': specifier: workspace:^ - version: link:../../ui/user-approval + version: link:../../interaction/user-approval '@deepseek-ai/dsh-user-interaction': specifier: workspace:^ - version: link:../../ui/user-interaction + version: link:../../interaction/user-interaction '@deepseek-ai/dsh-web': specifier: workspace:^ version: link:../../web/web @@ -1424,7 +1464,7 @@ importers: version: link:../../host/webserver '@deepseek-ai/dsh-session-projection-cache': specifier: workspace:^ - version: link:../../session-projection/session-projection-cache + version: link:../../session/session-projection-cache '@deepseek-ai/dsh-storage': specifier: workspace:^ version: link:../../storage/storage @@ -1458,7 +1498,7 @@ importers: dependencies: '@deepseek-ai/dsh-commands': specifier: workspace:^ - version: link:../../ui/commands + version: link:../../interaction/commands '@deepseek-ai/dsh-host-apiproxy': specifier: workspace:^ version: link:../../host/apiproxy @@ -1562,7 +1602,7 @@ importers: version: link:../ui-slots '@deepseek-ai/dsh-commands': specifier: workspace:^ - version: link:../../ui/commands + version: link:../../interaction/commands '@deepseek-ai/dsh-compact': specifier: workspace:^ version: link:../../compact/compact @@ -1580,10 +1620,10 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection '@deepseek-ai/dsh-session-title': specifier: workspace:^ - version: link:../../session-title/session-title + version: link:../../session/session-title immer: specifier: ^10.1.1 version: 10.2.0 @@ -1780,13 +1820,13 @@ importers: version: link:../../support/invariants '@deepseek-ai/dsh-permission': specifier: workspace:^ - version: link:../../ui/permission + version: link:../../interaction/permission '@deepseek-ai/dsh-plan-mode': specifier: workspace:^ version: link:../../plan/plan-mode '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection '@deepseek-ai/dsh-token-meter': specifier: workspace:^ version: link:../../llm/token-meter @@ -2024,7 +2064,7 @@ importers: version: link:../../support/invariants '@deepseek-ai/dsh-permission': specifier: workspace:^ - version: link:../../ui/permission + version: link:../../interaction/permission '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -2194,7 +2234,7 @@ importers: version: link:../../core/tools '@deepseek-ai/dsh-user-interaction': specifier: workspace:^ - version: link:../../ui/user-interaction + version: link:../../interaction/user-interaction '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -2740,7 +2780,7 @@ importers: version: link:../../core/agent '@deepseek-ai/dsh-commands': specifier: workspace:^ - version: link:../../ui/commands + version: link:../../interaction/commands '@deepseek-ai/dsh-compact': specifier: workspace:^ version: link:../compact @@ -2997,83 +3037,6 @@ importers: specifier: ^4.0.0-rc.6 version: link:../../../vendor/cordis - packages/cordis/repository-plugin: - dependencies: - zod: - specifier: ^4.4.3 - version: 4.4.3 - devDependencies: - '@cordisjs/plugin-loader': - specifier: workspace:^ - version: link:../../../vendor/loader - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-mcp-client': - specifier: workspace:^ - version: link:../../mcp/mcp-client - '@deepseek-ai/dsh-paths': - specifier: workspace:^ - version: link:../../util/paths - '@deepseek-ai/dsh-skill': - specifier: workspace:^ - version: link:../../skill/skill - '@deepseek-ai/dsh-skill-local': - specifier: workspace:^ - version: link:../../skill/skill-local - '@deepseek-ai/dsh-system-prompt': - specifier: workspace:^ - version: link:../../core/system-prompt - '@deepseek-ai/dsh-tools': - specifier: workspace:^ - version: link:../../core/tools - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/cordis/tool-cordis: - dependencies: - schemastery: - specifier: ^3.18.0 - version: link:../../../vendor/schemastery - devDependencies: - '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.5 - version: link:../../../vendor/loader - '@cordisjs/plugin-timer': - specifier: workspace:^ - version: link:../../../vendor/timer - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent - '@deepseek-ai/dsh-agent-loop': - specifier: workspace:^ - version: link:../../core/agent-loop - '@deepseek-ai/dsh-agent-loop-testkit': - specifier: workspace:^ - version: link:../../support/agent-loop-testkit - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-scope': - specifier: workspace:^ - version: link:../../core/scope - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - '@deepseek-ai/dsh-system-prompt': - specifier: workspace:^ - version: link:../../core/system-prompt - '@deepseek-ai/dsh-tools': - specifier: workspace:^ - version: link:../../core/tools - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - packages/core/agent: devDependencies: '@deepseek-ai/dsh-invariants': @@ -3124,10 +3087,10 @@ importers: version: link:../session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../system-prompt @@ -3219,7 +3182,7 @@ importers: version: link:../system-prompt '@deepseek-ai/dsh-user-approval': specifier: workspace:^ - version: link:../../ui/user-approval + version: link:../../interaction/user-approval cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -3282,6 +3245,9 @@ importers: '@deepseek-ai/dsh-loader-smoke': specifier: workspace:^ version: link:../../support/loader-smoke + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -3342,16 +3308,16 @@ importers: version: link:../agent-spine-demo '@deepseek-ai/dsh-app-boot': specifier: workspace:^ - version: link:../../ui/app-boot + version: link:../../boot/app-boot '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants '@deepseek-ai/dsh-session-checkpoint-policy': specifier: workspace:^ - version: link:../../session-persistence/session-checkpoint-policy + version: link:../../session/session-checkpoint-policy '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-session-query': specifier: workspace:^ version: link:../../session-query/session-query @@ -3439,7 +3405,7 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-title': specifier: workspace:^ - version: link:../../session-title/session-title + version: link:../../session/session-title '@deepseek-ai/dsh-skill': specifier: workspace:^ version: link:../../skill/skill @@ -3490,7 +3456,7 @@ importers: dependencies: '@deepseek-ai/dsh-app-boot': specifier: workspace:^ - version: link:../../ui/app-boot + version: link:../../boot/app-boot devDependencies: '@deepseek-ai/dsh-invariants': specifier: workspace:^ @@ -3512,7 +3478,7 @@ importers: version: link:../../core/agent '@deepseek-ai/dsh-commands': specifier: workspace:^ - version: link:../../ui/commands + version: link:../../interaction/commands '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants @@ -3655,7 +3621,7 @@ importers: version: link:../../core/tools '@deepseek-ai/dsh-user-approval': specifier: workspace:^ - version: link:../../ui/user-approval + version: link:../../interaction/user-approval cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -3762,7 +3728,7 @@ importers: version: link:../../core/agent '@deepseek-ai/dsh-commands': specifier: workspace:^ - version: link:../../ui/commands + version: link:../../interaction/commands '@deepseek-ai/dsh-goal': specifier: workspace:^ version: link:../goal @@ -3811,7 +3777,7 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection '@deepseek-ai/dsh-type-meta': specifier: workspace:^ version: link:../../typert/type-meta @@ -3917,6 +3883,24 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis + packages/guard/timeout-policy: + devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-timeout': + specifier: workspace:^ + version: link:../../util/timeout + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + packages/hooks/hook-protocol: devDependencies: '@deepseek-ai/dsh-bash': @@ -3967,10 +3951,10 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-subagent': specifier: workspace:^ version: link:../../subagent/subagent @@ -4019,10 +4003,10 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-subprocess-local': specifier: workspace:^ version: link:../../subprocess/subprocess-local @@ -4046,7 +4030,7 @@ importers: version: link:../../util/brand '@deepseek-ai/dsh-commands': specifier: workspace:^ - version: link:../../ui/commands + version: link:../../interaction/commands '@deepseek-ai/dsh-credentials': specifier: workspace:^ version: link:../../credentials/credentials @@ -4067,19 +4051,19 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection '@deepseek-ai/dsh-session-projection-cache': specifier: workspace:^ - version: link:../../session-projection/session-projection-cache + version: link:../../session/session-projection-cache '@deepseek-ai/dsh-session-query': specifier: workspace:^ version: link:../../session-query/session-query '@deepseek-ai/dsh-session-title': specifier: workspace:^ - version: link:../../session-title/session-title + version: link:../../session/session-title '@deepseek-ai/dsh-settings': specifier: workspace:^ version: link:../../settings/settings @@ -4094,10 +4078,10 @@ importers: version: link:../../core/tools '@deepseek-ai/dsh-user-approval': specifier: workspace:^ - version: link:../../ui/user-approval + version: link:../../interaction/user-approval '@deepseek-ai/dsh-user-interaction': specifier: workspace:^ - version: link:../../ui/user-interaction + version: link:../../interaction/user-interaction '@deepseek-ai/dsh-workspace': specifier: workspace:^ version: link:../../workspace/workspace @@ -4278,6 +4262,137 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis + packages/interaction/commands: + devDependencies: + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-brand': + specifier: workspace:^ + version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-scope': + specifier: workspace:^ + version: link:../../core/scope + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/interaction/permission: + dependencies: + schemastery: + specifier: ^3.18.0 + version: link:../../../vendor/schemastery + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@deepseek-ai/dsh-bash': + specifier: workspace:^ + version: link:../../bash/bash + '@deepseek-ai/dsh-commands': + specifier: workspace:^ + version: link:../commands + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-sandbox': + specifier: workspace:^ + version: link:../../sandbox/sandbox + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-projection': + specifier: workspace:^ + version: link:../../session/session-projection + '@deepseek-ai/dsh-settings': + specifier: workspace:^ + version: link:../../settings/settings + '@deepseek-ai/dsh-user-approval': + specifier: workspace:^ + version: link:../user-approval + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/interaction/tool-ask-user: + devDependencies: + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + '@deepseek-ai/dsh-user-interaction': + specifier: workspace:^ + version: link:../user-interaction + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/interaction/user-approval: + dependencies: + schemastery: + specifier: ^3.18.0 + version: link:../../../vendor/schemastery + devDependencies: + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-brand': + specifier: workspace:^ + version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-scope': + specifier: workspace:^ + version: link:../../core/scope + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/interaction/user-interaction: + devDependencies: + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + packages/llm/llm: dependencies: schemastery: @@ -4400,10 +4515,10 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-session-persistence-sqlite': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-sqlite + version: link:../../session/session-persistence-sqlite '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../core/system-prompt @@ -4440,7 +4555,7 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -4541,7 +4656,7 @@ importers: version: link:../../util/timeout '@deepseek-ai/dsh-timeout-policy': specifier: workspace:^ - version: link:../../timeout/timeout-policy + version: link:../../guard/timeout-policy '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools @@ -4600,7 +4715,7 @@ importers: version: link:../../code-runtime/code-runtime '@deepseek-ai/dsh-commands': specifier: workspace:^ - version: link:../../ui/commands + version: link:../../interaction/commands '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants @@ -4612,7 +4727,7 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../core/system-prompt @@ -4621,7 +4736,7 @@ importers: version: link:../../core/tools '@deepseek-ai/dsh-user-interaction': specifier: workspace:^ - version: link:../../ui/user-interaction + version: link:../../interaction/user-interaction cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -4918,7 +5033,25 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/sdk/create-sdk: + packages/scaffold/client: + devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-sdk-protocol': + specifier: workspace:^ + version: link:../protocol + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/scaffold/create-sdk: dependencies: '@deepseek-ai/dsh-helper': specifier: workspace:^ @@ -4934,7 +5067,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/sdk/helper: + packages/scaffold/helper: dependencies: '@clack/core': specifier: ^1.4.3 @@ -4966,10 +5099,10 @@ importers: version: link:../../support/invariants '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-session-persistence-sqlite': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-sqlite + version: link:../../session/session-persistence-sqlite '@deepseek-ai/dsh-subprocess': specifier: workspace:^ version: link:../../subprocess/subprocess @@ -4986,56 +5119,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/sdk/scripts: - dependencies: - '@deepseek-ai/dsh-helper': - specifier: workspace:^ - version: link:../helper - '@deepseek-ai/dsh-telemetry': - specifier: workspace:^ - version: link:../telemetry - commander: - specifier: ^15.0.0 - version: 15.0.0 - node-addon-require-builtin: - specifier: ^0.1.4 - version: 0.1.4 - devDependencies: - '@deepseek-ai/dsh-app-boot': - specifier: workspace:^ - version: link:../../ui/app-boot - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - tsdown: - specifier: ^0.22.2 - version: 0.22.2(oxc-resolver@11.20.0)(publint@0.3.21)(tsx@4.22.4)(typescript@6.0.3) - tsx: - specifier: ^4.22.4 - version: 4.22.4 - - packages/sdk/sdk-client: - devDependencies: - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-sdk-protocol': - specifier: workspace:^ - version: link:../sdk-protocol - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/sdk/sdk-protocol: + packages/scaffold/protocol: devDependencies: '@deepseek-ai/dsh-invariants': specifier: workspace:^ @@ -5053,7 +5137,81 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/sdk/telemetry: + packages/scaffold/scripts: + dependencies: + '@deepseek-ai/dsh-helper': + specifier: workspace:^ + version: link:../helper + '@deepseek-ai/dsh-telemetry': + specifier: workspace:^ + version: link:../telemetry + commander: + specifier: ^15.0.0 + version: 15.0.0 + node-addon-require-builtin: + specifier: ^0.1.4 + version: 0.1.4 + devDependencies: + '@deepseek-ai/dsh-app-boot': + specifier: workspace:^ + version: link:../../boot/app-boot + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + tsdown: + specifier: ^0.22.2 + version: 0.22.2(oxc-resolver@11.20.0)(publint@0.3.21)(tsx@4.22.4)(typescript@6.0.3) + tsx: + specifier: ^4.22.4 + version: 4.22.4 + + packages/scaffold/server: + dependencies: + schemastery: + specifier: ^3.17.0 + version: link:../../../vendor/schemastery + devDependencies: + '@cordisjs/plugin-loader': + specifier: workspace:^ + version: link:../../../vendor/loader + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-agent-spine-demo': + specifier: workspace:^ + version: link:../../examples/agent-spine-demo + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-llm-deepseek': + specifier: workspace:^ + version: link:../../llm/llm-deepseek + '@deepseek-ai/dsh-scope': + specifier: workspace:^ + version: link:../../core/scope + '@deepseek-ai/dsh-sdk-protocol': + specifier: workspace:^ + version: link:../protocol + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-persistence-jsonl': + specifier: workspace:^ + version: link:../../session/session-persistence-jsonl + '@deepseek-ai/dsh-subagent': + specifier: workspace:^ + version: link:../../subagent/subagent + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/scaffold/telemetry: dependencies: yaml: specifier: ^2.9.0 @@ -5072,7 +5230,185 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-persistence/session-checkpoint-policy: + packages/self-modification/repository-plugin: + dependencies: + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@cordisjs/plugin-loader': + specifier: workspace:^ + version: link:../../../vendor/loader + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-mcp-client': + specifier: workspace:^ + version: link:../../mcp/mcp-client + '@deepseek-ai/dsh-paths': + specifier: workspace:^ + version: link:../../util/paths + '@deepseek-ai/dsh-skill': + specifier: workspace:^ + version: link:../../skill/skill + '@deepseek-ai/dsh-skill-local': + specifier: workspace:^ + version: link:../../skill/skill-local + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/self-modification/tool-cordis: + dependencies: + schemastery: + specifier: ^3.18.0 + version: link:../../../vendor/schemastery + devDependencies: + '@cordisjs/plugin-loader': + specifier: ^1.0.0-rc.5 + version: link:../../../vendor/loader + '@cordisjs/plugin-timer': + specifier: workspace:^ + version: link:../../../vendor/timer + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-agent-loop': + specifier: workspace:^ + version: link:../../core/agent-loop + '@deepseek-ai/dsh-agent-loop-testkit': + specifier: workspace:^ + version: link:../../support/agent-loop-testkit + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-scope': + specifier: workspace:^ + version: link:../../core/scope + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/session-query/session-query: + devDependencies: + '@deepseek-ai/dsh-brand': + specifier: workspace:^ + version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-persistence': + specifier: workspace:^ + version: link:../../session/session-persistence + '@deepseek-ai/dsh-session-title': + specifier: workspace:^ + version: link:../../session/session-title + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/session-query/session-query-sqlite: + dependencies: + schemastery: + specifier: ^3.18.0 + version: link:../../../vendor/schemastery + devDependencies: + '@cordisjs/plugin-loader': + specifier: workspace:^ + version: link:../../../vendor/loader + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-persistence': + specifier: workspace:^ + version: link:../../session/session-persistence + '@deepseek-ai/dsh-session-persistence-sqlite': + specifier: workspace:^ + version: link:../../session/session-persistence-sqlite + '@deepseek-ai/dsh-session-query': + specifier: workspace:^ + version: link:../session-query + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/session-query/tool-session-query: + dependencies: + schemastery: + specifier: ^3.18.0 + version: link:../../../vendor/schemastery + devDependencies: + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-persistence': + specifier: workspace:^ + version: link:../../session/session-persistence + '@deepseek-ai/dsh-session-persistence-jsonl': + specifier: workspace:^ + version: link:../../session/session-persistence-jsonl + '@deepseek-ai/dsh-session-query': + specifier: workspace:^ + version: link:../session-query + '@deepseek-ai/dsh-session-query-sqlite': + specifier: workspace:^ + version: link:../session-query-sqlite + '@deepseek-ai/dsh-session-title': + specifier: workspace:^ + version: link:../../session/session-title + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + '@deepseek-ai/dsh-timeout': + specifier: workspace:^ + version: link:../../util/timeout + '@deepseek-ai/dsh-timeout-policy': + specifier: workspace:^ + version: link:../../guard/timeout-policy + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/session/session-checkpoint-policy: devDependencies: '@cordisjs/plugin-loader': specifier: workspace:^ @@ -5111,7 +5447,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-persistence/session-persistence: + packages/session/session-persistence: devDependencies: '@deepseek-ai/dsh-brand': specifier: workspace:^ @@ -5132,7 +5468,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-persistence/session-persistence-jsonl: + packages/session/session-persistence-jsonl: dependencies: koffi: specifier: ^3.1.0 @@ -5154,7 +5490,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-persistence/session-persistence-sqlite: + packages/session/session-persistence-sqlite: dependencies: schemastery: specifier: ^3.18.0 @@ -5173,7 +5509,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-projection/session-projection: + packages/session/session-projection: dependencies: zod: specifier: ^4.4.3 @@ -5189,7 +5525,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-projection/session-projection-cache: + packages/session/session-projection-cache: dependencies: schemastery: specifier: ^3.18.0 @@ -5206,7 +5542,7 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../session-persistence '@deepseek-ai/dsh-session-projection': specifier: workspace:^ version: link:../session-projection @@ -5220,63 +5556,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-query/session-query: - devDependencies: - '@deepseek-ai/dsh-brand': - specifier: workspace:^ - version: link:../../util/brand - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - '@deepseek-ai/dsh-session-persistence': - specifier: workspace:^ - version: link:../../session-persistence/session-persistence - '@deepseek-ai/dsh-session-title': - specifier: workspace:^ - version: link:../../session-title/session-title - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/session-query/session-query-sqlite: - dependencies: - schemastery: - specifier: ^3.18.0 - version: link:../../../vendor/schemastery - devDependencies: - '@cordisjs/plugin-loader': - specifier: workspace:^ - version: link:../../../vendor/loader - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - '@deepseek-ai/dsh-session-persistence': - specifier: workspace:^ - version: link:../../session-persistence/session-persistence - '@deepseek-ai/dsh-session-persistence-sqlite': - specifier: workspace:^ - version: link:../../session-persistence/session-persistence-sqlite - '@deepseek-ai/dsh-session-query': - specifier: workspace:^ - version: link:../session-query - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/session-query/tool-session-query: - dependencies: - schemastery: - specifier: ^3.18.0 - version: link:../../../vendor/schemastery + packages/session/session-telemetry: devDependencies: '@deepseek-ai/dsh-agent': specifier: workspace:^ @@ -5284,44 +5564,66 @@ importers: '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session - '@deepseek-ai/dsh-session-persistence': - specifier: workspace:^ - version: link:../../session-persistence/session-persistence - '@deepseek-ai/dsh-session-persistence-jsonl': - specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl - '@deepseek-ai/dsh-session-query': - specifier: workspace:^ - version: link:../session-query - '@deepseek-ai/dsh-session-query-sqlite': - specifier: workspace:^ - version: link:../session-query-sqlite - '@deepseek-ai/dsh-session-title': - specifier: workspace:^ - version: link:../../session-title/session-title - '@deepseek-ai/dsh-system-prompt': - specifier: workspace:^ - version: link:../../core/system-prompt - '@deepseek-ai/dsh-timeout': - specifier: workspace:^ - version: link:../../util/timeout - '@deepseek-ai/dsh-timeout-policy': - specifier: workspace:^ - version: link:../../timeout/timeout-policy - '@deepseek-ai/dsh-tools': - specifier: workspace:^ - version: link:../../core/tools cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-title/session-title: + packages/session/session-telemetry-otel: + dependencies: + '@opentelemetry/api': + specifier: ^1.9.1 + version: 1.9.1 + '@opentelemetry/api-logs': + specifier: ^0.220.0 + version: 0.220.0 + '@opentelemetry/exporter-logs-otlp-http': + specifier: ^0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': + specifier: ^0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ^2.9.0 + version: 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': + specifier: ^0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.1) + schemastery: + specifier: ^3.18.0 + version: link:../../../vendor/schemastery + devDependencies: + '@cordisjs/plugin-loader': + specifier: workspace:^ + version: link:../../../vendor/loader + '@deepseek-ai/dsh-brand': + specifier: workspace:^ + version: link:../../util/brand + '@deepseek-ai/dsh-command-feedback': + specifier: workspace:^ + version: link:../../feedback/command-feedback + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-paths': + specifier: workspace:^ + version: link:../../util/paths + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-telemetry': + specifier: workspace:^ + version: link:../session-telemetry + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + + packages/session/session-title: dependencies: schemastery: specifier: ^3.18.0 @@ -5344,18 +5646,18 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../session-persistence-jsonl '@deepseek-ai/dsh-session-persistence-sqlite': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-sqlite + version: link:../session-persistence-sqlite '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../session-projection cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-title/session-title-all-messages-llm: + packages/session/session-title-all-messages-llm: dependencies: schemastery: specifier: ^3.18.0 @@ -5380,7 +5682,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-title/session-title-first-message-llm: + packages/session/session-title-first-message-llm: dependencies: schemastery: specifier: ^3.18.0 @@ -5414,7 +5716,7 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/session-title/session-title-llm: + packages/session/session-title-llm: dependencies: schemastery: specifier: ^3.18.0 @@ -5735,13 +6037,13 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection '@deepseek-ai/dsh-session-projection-cache': specifier: workspace:^ - version: link:../../session-projection/session-projection-cache + version: link:../../session/session-projection-cache '@deepseek-ai/dsh-storage': specifier: workspace:^ version: link:../../storage/storage @@ -5867,7 +6169,7 @@ importers: version: link:../../support/loader-smoke '@deepseek-ai/dsh-sdk-protocol': specifier: workspace:^ - version: link:../../sdk/sdk-protocol + version: link:../../scaffold/protocol '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -5913,10 +6215,10 @@ importers: version: link:../../support/loader-smoke '@deepseek-ai/dsh-sdk-client': specifier: workspace:^ - version: link:../../sdk/sdk-client + version: link:../../scaffold/client '@deepseek-ai/dsh-sdk-protocol': specifier: workspace:^ - version: link:../../sdk/sdk-protocol + version: link:../../scaffold/protocol '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -6010,7 +6312,7 @@ importers: version: link:../../core/tools '@deepseek-ai/dsh-user-approval': specifier: workspace:^ - version: link:../../ui/user-approval + version: link:../../interaction/user-approval cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -6090,10 +6392,10 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-subagent': specifier: workspace:^ version: link:../subagent @@ -6141,13 +6443,13 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection '@deepseek-ai/dsh-subagent': specifier: workspace:^ version: link:../subagent @@ -6187,10 +6489,10 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl + version: link:../../session/session-persistence-jsonl '@deepseek-ai/dsh-subagent': specifier: workspace:^ version: link:../subagent @@ -6425,91 +6727,6 @@ importers: specifier: ^4.0.0-rc.6 version: link:../../../vendor/cordis - packages/telemetry/session-telemetry: - devDependencies: - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/telemetry/session-telemetry-otel: - dependencies: - '@opentelemetry/api': - specifier: ^1.9.1 - version: 1.9.1 - '@opentelemetry/api-logs': - specifier: ^0.220.0 - version: 0.220.0 - '@opentelemetry/exporter-logs-otlp-http': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': - specifier: ^2.9.0 - version: 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) - schemastery: - specifier: ^3.18.0 - version: link:../../../vendor/schemastery - devDependencies: - '@cordisjs/plugin-loader': - specifier: workspace:^ - version: link:../../../vendor/loader - '@deepseek-ai/dsh-brand': - specifier: workspace:^ - version: link:../../util/brand - '@deepseek-ai/dsh-command-feedback': - specifier: workspace:^ - version: link:../../feedback/command-feedback - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-paths': - specifier: workspace:^ - version: link:../../util/paths - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - '@deepseek-ai/dsh-session-telemetry': - specifier: workspace:^ - version: link:../session-telemetry - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/timeout/timeout-policy: - devDependencies: - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-timeout': - specifier: workspace:^ - version: link:../../util/timeout - '@deepseek-ai/dsh-tools': - specifier: workspace:^ - version: link:../../core/tools - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - packages/todo/tool-todo: dependencies: schemastery: @@ -6548,7 +6765,7 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../session-projection/session-projection + version: link:../../session/session-projection '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../core/system-prompt @@ -6557,7 +6774,7 @@ importers: version: link:../../core/tools '@deepseek-ai/dsh-user-interaction': specifier: workspace:^ - version: link:../../ui/user-interaction + version: link:../../interaction/user-interaction cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis @@ -6576,7 +6793,7 @@ importers: version: link:../../support/invariants '@deepseek-ai/dsh-tool-cordis': specifier: workspace:^ - version: link:../../cordis/tool-cordis + version: link:../../self-modification/tool-cordis '@deepseek-ai/dsh-typert-registry': specifier: workspace:^ version: link:../registry @@ -6634,220 +6851,6 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/ui/app-boot: - dependencies: - js-yaml: - specifier: ^4.2.0 - version: 4.2.0 - devDependencies: - '@cordisjs/plugin-group': - specifier: workspace:^ - version: link:../../../vendor/group - '@cordisjs/plugin-hmr': - specifier: workspace:^ - version: link:../../../vendor/hmr - '@cordisjs/plugin-include': - specifier: workspace:^ - version: link:../../../vendor/include - '@cordisjs/plugin-loader': - specifier: workspace:^ - version: link:../../../vendor/loader - '@cordisjs/plugin-timer': - specifier: workspace:^ - version: link:../../../vendor/timer - '@deepseek-ai/dsh-environment': - specifier: workspace:^ - version: link:../../util/environment - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-paths': - specifier: workspace:^ - version: link:../../util/paths - '@deepseek-ai/dsh-system-prompt': - specifier: workspace:^ - version: link:../../core/system-prompt - '@types/js-yaml': - specifier: ^4.0.9 - version: 4.0.9 - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/ui/commands: - devDependencies: - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent - '@deepseek-ai/dsh-brand': - specifier: workspace:^ - version: link:../../util/brand - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-scope': - specifier: workspace:^ - version: link:../../core/scope - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/ui/jsonrpc: - dependencies: - schemastery: - specifier: ^3.17.0 - version: link:../../../vendor/schemastery - devDependencies: - '@cordisjs/plugin-loader': - specifier: workspace:^ - version: link:../../../vendor/loader - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent - '@deepseek-ai/dsh-agent-spine-demo': - specifier: workspace:^ - version: link:../../examples/agent-spine-demo - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-llm-deepseek': - specifier: workspace:^ - version: link:../../llm/llm-deepseek - '@deepseek-ai/dsh-scope': - specifier: workspace:^ - version: link:../../core/scope - '@deepseek-ai/dsh-sdk-protocol': - specifier: workspace:^ - version: link:../../sdk/sdk-protocol - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - '@deepseek-ai/dsh-session-persistence-jsonl': - specifier: workspace:^ - version: link:../../session-persistence/session-persistence-jsonl - '@deepseek-ai/dsh-subagent': - specifier: workspace:^ - version: link:../../subagent/subagent - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/ui/permission: - dependencies: - schemastery: - specifier: ^3.18.0 - version: link:../../../vendor/schemastery - zod: - specifier: ^4.4.3 - version: 4.4.3 - devDependencies: - '@deepseek-ai/dsh-bash': - specifier: workspace:^ - version: link:../../bash/bash - '@deepseek-ai/dsh-commands': - specifier: workspace:^ - version: link:../commands - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-sandbox': - specifier: workspace:^ - version: link:../../sandbox/sandbox - '@deepseek-ai/dsh-sandbox-policy': - specifier: workspace:^ - version: link:../../sandbox/sandbox-policy - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - '@deepseek-ai/dsh-session-projection': - specifier: workspace:^ - version: link:../../session-projection/session-projection - '@deepseek-ai/dsh-settings': - specifier: workspace:^ - version: link:../../settings/settings - '@deepseek-ai/dsh-user-approval': - specifier: workspace:^ - version: link:../user-approval - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/ui/tool-ask-user: - devDependencies: - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-system-prompt': - specifier: workspace:^ - version: link:../../core/system-prompt - '@deepseek-ai/dsh-tools': - specifier: workspace:^ - version: link:../../core/tools - '@deepseek-ai/dsh-user-interaction': - specifier: workspace:^ - version: link:../user-interaction - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/ui/user-approval: - dependencies: - schemastery: - specifier: ^3.18.0 - version: link:../../../vendor/schemastery - devDependencies: - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent - '@deepseek-ai/dsh-brand': - specifier: workspace:^ - version: link:../../util/brand - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-scope': - specifier: workspace:^ - version: link:../../core/scope - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - '@deepseek-ai/dsh-system-prompt': - specifier: workspace:^ - version: link:../../core/system-prompt - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - - packages/ui/user-interaction: - devDependencies: - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - packages/util/atomic-write: devDependencies: '@deepseek-ai/dsh-invariants': @@ -6946,7 +6949,7 @@ importers: version: link:../../core/system-prompt '@deepseek-ai/dsh-timeout-policy': specifier: workspace:^ - version: link:../../timeout/timeout-policy + version: link:../../guard/timeout-policy '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools @@ -7246,7 +7249,7 @@ importers: version: link:../../core/session '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../session-persistence/session-persistence + version: link:../../session/session-persistence '@deepseek-ai/dsh-storage': specifier: workspace:^ version: link:../../storage/storage @@ -7285,7 +7288,7 @@ importers: version: link:../../packages/examples/agent-spine-demo '@deepseek-ai/dsh-app-boot': specifier: workspace:^ - version: link:../../packages/ui/app-boot + version: link:../../packages/boot/app-boot '@deepseek-ai/dsh-bash': specifier: workspace:^ version: link:../../packages/bash/bash @@ -7309,7 +7312,7 @@ importers: version: link:../../packages/goal/command-goal '@deepseek-ai/dsh-commands': specifier: workspace:^ - version: link:../../packages/ui/commands + version: link:../../packages/interaction/commands '@deepseek-ai/dsh-compact': specifier: workspace:^ version: link:../../packages/compact/compact @@ -7354,7 +7357,7 @@ importers: version: link:../../packages/support/invariants '@deepseek-ai/dsh-jsonrpc': specifier: workspace:^ - version: link:../../packages/ui/jsonrpc + version: link:../../packages/scaffold/server '@deepseek-ai/dsh-jsonrpc-demo': specifier: workspace:^ version: link:../../packages/examples/jsonrpc-demo @@ -7375,7 +7378,7 @@ importers: version: link:../../packages/util/paths '@deepseek-ai/dsh-permission': specifier: workspace:^ - version: link:../../packages/ui/permission + version: link:../../packages/interaction/permission '@deepseek-ai/dsh-plan-mode': specifier: workspace:^ version: link:../../packages/plan/plan-mode @@ -7405,25 +7408,25 @@ importers: version: link:../../packages/core/scope '@deepseek-ai/dsh-sdk-protocol': specifier: workspace:^ - version: link:../../packages/sdk/sdk-protocol + version: link:../../packages/scaffold/protocol '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../packages/core/session '@deepseek-ai/dsh-session-checkpoint-policy': specifier: workspace:^ - version: link:../../packages/session-persistence/session-checkpoint-policy + version: link:../../packages/session/session-checkpoint-policy '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ - version: link:../../packages/session-persistence/session-persistence + version: link:../../packages/session/session-persistence '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ - version: link:../../packages/session-persistence/session-persistence-jsonl + version: link:../../packages/session/session-persistence-jsonl '@deepseek-ai/dsh-session-persistence-sqlite': specifier: workspace:^ - version: link:../../packages/session-persistence/session-persistence-sqlite + version: link:../../packages/session/session-persistence-sqlite '@deepseek-ai/dsh-session-projection': specifier: workspace:^ - version: link:../../packages/session-projection/session-projection + version: link:../../packages/session/session-projection '@deepseek-ai/dsh-session-query': specifier: workspace:^ version: link:../../packages/session-query/session-query @@ -7435,7 +7438,7 @@ importers: version: link:../../packages/context/session-reference '@deepseek-ai/dsh-session-title': specifier: workspace:^ - version: link:../../packages/session-title/session-title + version: link:../../packages/session/session-title '@deepseek-ai/dsh-settings': specifier: workspace:^ version: link:../../packages/settings/settings @@ -7480,13 +7483,13 @@ importers: version: link:../../packages/util/timeout '@deepseek-ai/dsh-timeout-policy': specifier: workspace:^ - version: link:../../packages/timeout/timeout-policy + version: link:../../packages/guard/timeout-policy '@deepseek-ai/dsh-token-meter': specifier: workspace:^ version: link:../../packages/llm/token-meter '@deepseek-ai/dsh-tool-ask-user': specifier: workspace:^ - version: link:../../packages/ui/tool-ask-user + version: link:../../packages/interaction/tool-ask-user '@deepseek-ai/dsh-tool-bash': specifier: workspace:^ version: link:../../packages/bash/tool-bash @@ -7495,7 +7498,7 @@ importers: version: link:../../packages/pty/tool-bash-persistent '@deepseek-ai/dsh-tool-cordis': specifier: workspace:^ - version: link:../../packages/cordis/tool-cordis + version: link:../../packages/self-modification/tool-cordis '@deepseek-ai/dsh-tool-fs': specifier: workspace:^ version: link:../../packages/fs/tool-fs @@ -7534,10 +7537,10 @@ importers: version: link:../../packages/typert/type-meta '@deepseek-ai/dsh-user-approval': specifier: workspace:^ - version: link:../../packages/ui/user-approval + version: link:../../packages/interaction/user-approval '@deepseek-ai/dsh-user-interaction': specifier: workspace:^ - version: link:../../packages/ui/user-interaction + version: link:../../packages/interaction/user-interaction '@deepseek-ai/dsh-web': specifier: workspace:^ version: link:../../packages/web/web @@ -14303,7 +14306,7 @@ snapshots: '@opentelemetry/api-logs@0.220.0': dependencies: - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.0': {} diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index baeac7a8c0..0395fe8c36 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -28,6 +28,33 @@ describe('CI workflow', () => { }) }) +describe('E2B e2e workflow', () => { + it('is manual-only and fails loud before running the focused live suite', () => { + const workflow = loadWorkflow('.github/workflows/e2b-e2e.yml') + expect(workflow.on).toEqual({ workflow_dispatch: null }) + if (!isRecord(workflow.jobs) || !isRecord(workflow.jobs.e2b) || !Array.isArray(workflow.jobs.e2b.steps)) { + throw new TypeError('E2B e2e workflow must define the e2b job steps') + } + + const steps = workflow.jobs.e2b.steps.filter(isRecord) + const preflight = steps.find(step => step.name === 'Preflight (require E2B API key)') + const e2b = steps.find(step => step.name === 'E2B tests (live sandbox)') + + expect(preflight).toMatchObject({ + env: { E2B_API_KEY: '${{ secrets.E2B_API_KEY_EXTERNAL }}' }, + }) + expect(preflight?.run).toContain('E2B_API_KEY_EXTERNAL repository secret') + expect(e2b).toMatchObject({ + env: { + E2B_API_KEY: '${{ secrets.E2B_API_KEY_EXTERNAL }}', + DSH_E2E_MAX_WORKERS: '1', + DSH_EXAMPLE_MODE: 'lib', + }, + }) + expect(e2b?.run).toContain('packages/e2b/e2b/tests/composition.e2e.ts') + }) +}) + describe('Issue lifecycle workflow', () => { it('uses review signals instead of rerunning when a draft becomes ready', () => { const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml') 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 920688df6d..a79ffdd47f 100644 --- a/scripts/gen-config-catalog.ts +++ b/scripts/gen-config-catalog.ts @@ -777,7 +777,7 @@ function requiresLine(inject: string[]): string { } /** Render one reference as a link: another plugin's config type → its section, - * a curated core-data-structures name → its page, any other workspace type → + * a curated subsystems name → its page, any other workspace type → * its source file, an external type → named with its module, unlinked. */ function refLink(ref: TypeRef, byName: Map): string { const target = byName.get(ref.specifier) @@ -785,7 +785,7 @@ function refLink(ref: TypeRef, byName: Map): string { return `[\`${ref.alias}\`](#${slug(target.pkg)})` } const page = LINK_MAP[ref.imported] - if (page) return `[\`${ref.alias}\`](core-data-structures/${page})` + if (page) return `[\`${ref.alias}\`](subsystems/${page})` if (target) return `[\`${ref.alias}\`](../${target.entry})` return `\`${ref.alias}\` (\`${ref.specifier}\`)` } @@ -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 [core-data-structures/](core-data-structures/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 46f8514153..8ef7fc2878 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -1,51 +1,187 @@ /** - * 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 OUT_RUNTIME_API = 'packages/cordis/tool-cordis/src/api-catalog.ts' +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 core-data-structures 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', + agentPresets: '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', + AgentFactory: 'core.md', + AgentHandle: 'core.md', AgentOptions: 'core.md', AgentStatus: 'core.md', - ContentBlock: 'core.md', - ContinuationDecision: 'core.md', - ContinuationStop: 'core.md', - GenerateOptions: 'core.md', - MessageId: 'core.md', - HookContext: 'core.md', + ContentBlock: 'llm-streaming.md', + CreateAgentOptions: 'core.md', + GenerateOptions: 'llm-streaming.md', + InboxItem: 'core.md', + InboxPlacement: 'core.md', + MessageId: 'llm-streaming.md', + ResumeAgentOptions: 'core.md', SettleReason: 'core.md', - AdapterRegistrationHandle: 'core.md', - DirectoryRegistrationHandle: 'core.md', - LlmCallConfig: 'core.md', - LlmModelContext: 'core.md', - LlmModelReasoningInfo: 'core.md', - LlmResolvedModelInfo: 'core.md', + AdapterRegistrationHandle: 'llm-streaming.md', + DirectoryRegistrationHandle: 'llm-streaming.md', + LlmCallConfig: 'llm-streaming.md', + LlmModelContext: 'llm-streaming.md', + LlmModelReasoningInfo: 'llm-streaming.md', + LlmResolvedModelInfo: 'llm-streaming.md', LlmFailure: 'llm-streaming.md', - LlmModelInfo: 'core.md', - LlmProviderInfo: 'core.md', - LlmConfigurableProvider: 'core.md', - LlmModelDiscoveryRequest: 'core.md', - LlmDiscoveredModel: 'core.md', + LlmModelInfo: 'llm-streaming.md', + LlmProviderInfo: 'llm-streaming.md', + LlmConfigurableProvider: 'llm-streaming.md', + LlmModelDiscoveryRequest: 'llm-streaming.md', + LlmDiscoveredModel: 'llm-streaming.md', ResolvedRetryPolicy: 'llm-streaming.md', - Message: 'core.md', - MessageSource: 'core.md', + Message: 'llm-streaming.md', + MessageSource: 'llm-streaming.md', UserMessage: 'session.md', PreStepDecision: 'core.md', PreStepContext: 'core.md', @@ -54,7 +190,7 @@ export const LINK_MAP: Readonly> = { PreparedReferencedMessage: 'session-reference.md', SessionReferenceCandidate: 'session-reference.md', SessionReferenceInput: 'session-reference.md', - SessionEvent: 'core.md', + SessionEvent: 'session.md', SessionId: 'core.md', SessionStartSource: 'core.md', SessionLogSnapshot: 'session-query.md', @@ -92,12 +228,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', @@ -230,8 +366,34 @@ export const LINK_MAP: Readonly> = { WebSearchRequest: 'web.md', WebSearchResult: 'web.md', WorkflowRun: 'workflow.md', + PresetOption: 'permission.md', + PresetSpec: 'permission.md', + InvariantInstaller: 'invariants.md', + WebRoute: 'http-server.md', + StorageBackend: 'storage.md', + StorageForms: 'storage.md', + Domain: 'storage.md', + DomainSpec: 'storage.md', + DomainChanged: 'storage.md', + DomainFacility: 'storage.md', + Workspace: 'workspace.md', + WorkspaceId: 'workspace.md', + WebBootGraph: 'client-modules.md', + 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. */ @@ -248,9 +410,8 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet = new Set([ 'Readonly', ]) -/** Project types deliberately documented outside the core-data catalog. */ +/** Project types deliberately documented outside the subsystems catalog. */ export const TYPE_LINK_EXEMPTIONS: Readonly> = { - AgentFactory: 'agent creation seam is owned by packages/core/agent/README.md', z: 'schemastery schema constructor is owned by vendor/schemastery (vendored upstream)', BeginCommandRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts', InsertReferenceRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts', @@ -262,56 +423,28 @@ 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', - Domain: 'domain interface is owned by packages/storage/storage-domain/README.md', - DomainChanged: 'event-local snapshot is owned by packages/storage/storage-domain/src/events.ts', - DomainFacility: 'domain form facility is owned by packages/storage/storage-domain/README.md', DomainImpl: 'domain implementation contract is owned by packages/storage/storage-domain/README.md', - DomainSpec: 'domain declaration contract is owned by packages/storage/storage-domain/README.md', - StorageBackend: 'backend contract is owned by packages/storage/storage/src/backend.ts', - StorageForms: 'merge-extensible form map is owned by packages/storage/storage/src/index.ts', - ProjectionDefinition: 'projection unit contract is owned by packages/session-projection/session-projection/README.md', - SessionProjectionMap: 'merge-extensible projection key map is owned by packages/session-projection/session-projection/src/types.ts', - ProjectionChangeListener: 'change-feed listener contract is owned by packages/session-projection/session-projection/src/index.ts', - ProjectionSnapshot: 'watermark snapshot shape is owned by packages/session-projection/session-projection/src/index.ts', - ProjectionCheckpoint: 'persisted checkpoint row map is owned by packages/session-projection/session-projection/src/index.ts', - CommandExecution: 'executor return contract is owned by packages/ui/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', + CommandExecution: 'executor return contract is owned by packages/interaction/commands/src/index.ts', 'z.core.JSONSchema.BaseSchema': 'zod projection output is owned by the zod v4 API', 'z.core.ToJSONSchemaParams': 'zod projection parameters are owned by the zod v4 API', - InvariantInstaller: 'service-local contribution contract is owned by packages/support/invariants/README.md', + 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', - WebBootGraph: 'web boot graph wire shape is owned by packages/client/modules/src/client/index.ts', - WebRoute: 'route registration contract is owned by packages/host/webserver/src/index.ts', - WebUpgradeRoute: - 'upgrade route registration contract is owned by packages/host/webserver/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', - PresetOption: 'deployment menu metadata is owned by packages/ui/permission/README.md', - PresetSpec: 'deployment preset composition is owned by packages/ui/permission/README.md', - KnobState: 'projection unit state shape is owned by packages/ui/permission/README.md', - PermissionSelect: 'permissions projection payload is owned by packages/ui/permission/src/types.ts', + KnobState: 'projection unit state shape is owned by packages/interaction/permission/README.md', + PermissionSelect: 'permissions projection payload is owned by packages/interaction/permission/src/types.ts', PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md', - ResumeAgentOptions: 'agent resume contract is owned by packages/core/agent/README.md', Sandbox: 'external E2B SDK handle is owned by packages/e2b/e2b/README.md', SessionForkSource: 'service-local fork input is owned by packages/core/session/src/index.ts', SubagentRunEndInfo: 'event payload contract is owned by packages/subagent/subagent/src/types.ts', SubagentRunInfo: 'event payload contract is owned by packages/subagent/subagent/src/types.ts', - TelemetryRecord: 'seam-local record contract is owned by packages/telemetry/session-telemetry/src/index.ts', WorkflowAgentEndInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts', WorkflowAgentInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts', WorkflowResultInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts', - Workspace: 'workspace entity contract is owned by packages/workspace/workspace/README.md', - WorkspaceId: 'branded id is owned by packages/workspace/workspace/README.md', } /** Repository data policy consumed by the Cordis catalog projector. */ @@ -329,8 +462,7 @@ export const CORDIS_CATALOG_POLICY: CordisCatalogPolicy = { { name: 'internal/listener', summary: 'A listener was registered.', source: 'vendor/cordis/src/events.ts:340' }, { name: 'internal/dispatch', summary: 'An event is being dispatched to listeners.', source: 'vendor/cordis/src/events.ts:342' }, { name: 'hmr/change', summary: 'A watched source file changed on disk.', source: 'vendor/hmr/src/index.ts:20' }, - { name: 'hmr/reload', summary: 'Plugins are being reloaded after a change.', source: 'vendor/hmr/src/index.ts:22' }, - { name: 'hmr/config-update-failed', summary: 'A watched config-file refresh failed.', source: 'vendor/hmr/src/index.ts:29' }, + { name: 'hmr/reload', summary: 'Plugins are being reloaded after a change.', source: 'vendor/hmr/src/index.ts:21' }, { name: 'exit', summary: 'The process is exiting on a signal.', source: 'vendor/loader/src/index.ts:23' }, { name: 'loader/config-update', summary: 'The loader config tree changed.', source: 'vendor/loader/src/index.ts:24' }, { name: 'loader/entry-init', summary: 'A config entry is being initialized.', source: 'vendor/loader/src/index.ts:25' }, @@ -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 ddd5435363..edc99ff31c 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -1351,7 +1351,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 [core-data-structures/](core-data-structures/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 685e4b65ef..5294242e27 100644 --- a/scripts/gen-persistence-catalog.ts +++ b/scripts/gen-persistence-catalog.ts @@ -31,7 +31,7 @@ const EVENT_ENVELOPE_TYPE_NAMES = [ type EventEnvelopeTypeName = typeof EVENT_ENVELOPE_TYPE_NAMES[number] -/** Primary core-data-structures page for linked payload types. */ +/** Primary subsystems page for linked payload types. */ const LINK_MAP: Record = { CallId: 'core.md', ContentBlock: 'core.md', @@ -330,7 +330,7 @@ function typeLinks(payload: string): string { if (new RegExp(`\\b${name}\\b`).test(payload)) seen.add(name) } if (seen.size === 0) return '' - const links = [...seen].sort().map(n => `[${n}](core-data-structures/${LINK_MAP[n]})`) + const links = [...seen].sort().map(n => `[${n}](subsystems/${LINK_MAP[n]})`) return `Types: ${links.join(' · ')}` } @@ -352,11 +352,11 @@ 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](core-data-structures/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](core-data-structures/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).', '', - 'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](core-data-structures/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.', + 'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.', '', '## Event envelope', '', diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts index 8299114ff5..c9b1f41dbf 100644 --- a/scripts/gen-third-party-notices.spec.ts +++ b/scripts/gen-third-party-notices.spec.ts @@ -70,7 +70,7 @@ describe('tierExternalDeps', () => { it('keeps a package runtime when any shipping area declares it, and excludes workspace links', () => { const { manifests, names } = workspace({ 'package.json': { devDependencies: { shared: '^1' } }, - 'packages/ui/tui/package.json': { name: '@deepseek-ai/dsh-tui', dependencies: { shared: '^1', '@deepseek-ai/dsh-cli': 'workspace:^' } }, + 'packages/interaction/tui/package.json': { name: '@deepseek-ai/dsh-tui', dependencies: { shared: '^1', '@deepseek-ai/dsh-cli': 'workspace:^' } }, 'apps/cli/package.json': { name: '@deepseek-ai/dsh-cli' }, }) diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index b59aafe5f0..a68ab874f0 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -154,7 +154,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ { pkg: '@deepseek-ai/dsh-tool-ask-user', dir: 'tool-ask-user', - source: 'packages/ui/tool-ask-user/src/index.ts', + source: 'packages/interaction/tool-ask-user/src/index.ts', requires: ['ctx.tools', 'ctx.userInteraction'], writes: ['tool/call', 'tool/result after a UI/provider answers the question'], async mount(ctx) { @@ -226,7 +226,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ { pkg: '@deepseek-ai/dsh-tool-cordis', dir: 'tool-cordis', - source: 'packages/cordis/tool-cordis/src/index.ts', + source: 'packages/self-modification/tool-cordis/src/index.ts', requires: ['ctx.tools'], writes: ['tool/call', 'tool/result', 'process-local temporary Plugin lifecycle'], async mount(ctx) { @@ -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 [core-data-structures/](core-data-structures/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/install-lefthook.mjs b/scripts/install-lefthook.mjs index 0f840de9c8..198f428b0a 100644 --- a/scripts/install-lefthook.mjs +++ b/scripts/install-lefthook.mjs @@ -27,6 +27,19 @@ const INSTALL_LOCK_INITIALIZATION_TIMEOUT_MS = 1_000 const INSTALL_LOCK_POLL_MS = 50 const ALLOW_HOOKS_PATH_OVERRIDE = 'DSH_LEFTHOOK_ALLOW_HOOKS_PATH_OVERRIDE' const REPOSITORY_EXTENSION_PATTERN = '^extensions\\.' +const PAIRING_MERGE_DRIVER_CONFIG = [ + ['merge.dsh-translation-pairing.name', 'DeepSeek Harness bilingual pairing records'], + [ + 'merge.dsh-translation-pairing.driver', + 'scripts/merge-translation-pairing-driver.sh %O %A %B %P', + ], +] +const PAIRING_MERGE_DRIVER_PROBE = [ + '--import', + 'tsx/esm', + 'scripts/merge-translation-pairing.ts', + '--probe', +] function errorCode(error) { return typeof error === 'object' && error !== null && 'code' in error @@ -595,6 +608,86 @@ function refuseScopedHooksPath(entry) { ) } +function installPairingMergeDriver(root, worktreeConfigPath) { + const added = [] + try { + for (const [key, expected] of PAIRING_MERGE_DRIVER_CONFIG) { + const entries = includedFileConfigEntries(root, worktreeConfigPath, key) + const includedEntry = entries.find(entry => !originIsFile(entry.origin, root, worktreeConfigPath)) + if (includedEntry !== undefined) { + throw new Error( + `refusing pairing merge-driver config from an included worktree file (${configSource(includedEntry)})`, + ) + } + const existing = assertSingle(entries.map(entry => entry.value), `worktree ${key}`) + const effectiveBefore = effectiveConfigEntry(root, key) + if (effectiveBefore?.scope === 'command') { + throw new Error( + `refusing command-scoped ${key} (${configSource(effectiveBefore)}); ` + + 'transient configuration cannot be replaced by the worktree installer', + ) + } + if (existing === undefined && effectiveBefore !== undefined && effectiveBefore.value !== expected) { + throw new Error( + `refusing to mask inherited ${key} (${configSource(effectiveBefore)}); ` + + 'remove or integrate the custom pairing merge driver explicitly', + ) + } + if (existing !== undefined && existing !== expected) { + throw new Error( + `refusing to replace worktree ${key} value ${JSON.stringify(existing)}; ` + + 'remove or integrate the custom pairing merge driver explicitly', + ) + } + if (existing === undefined) { + git(['config', '--worktree', key, expected], root) + added.push(key) + } + const installed = includedFileConfigEntries(root, worktreeConfigPath, key) + if ( + installed.length !== 1 + || installed[0]?.value !== expected + || !originIsFile(installed[0].origin, root, worktreeConfigPath) + ) { + throw new Error(`new worktree-local ${key} did not become the direct worktree value`) + } + const effectiveAfter = effectiveConfigEntry(root, key) + if ( + effectiveAfter === undefined + || effectiveAfter.scope !== 'worktree' + || effectiveAfter.value !== expected + || !originIsFile(effectiveAfter.origin, root, worktreeConfigPath) + ) { + throw new Error(`new worktree-local ${key} did not become the effective direct worktree value`) + } + } + } catch (error) { + const rollbackErrors = [] + for (const key of added.reverse()) { + try { + git(['config', '--worktree', '--unset-all', key], root) + } catch (rollbackError) { + rollbackErrors.push(rollbackError) + } + } + if (rollbackErrors.length > 0) { + throw new AggregateError( + [error, ...rollbackErrors], + `Pairing merge-driver configuration failed: ${String(error)}; ` + + `rollback also failed: ${rollbackErrors.map(String).join('; ')}`, + ) + } + throw error + } + return () => { + for (const key of added.reverse()) git(['config', '--worktree', '--unset-all', key], root) + } +} + +function probePairingMergeDriver(root) { + capture(process.execPath, PAIRING_MERGE_DRIVER_PROBE, { cwd: root }) +} + async function main() { if (process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true') return if (typeof lefthookPackage.bin?.lefthook !== 'string') return @@ -682,7 +775,10 @@ async function main() { applyWorktreeConfigMigration(root, commonConfigPath, migration) let pathChanged = false + let rollbackPairingMergeDriver = () => {} try { + probePairingMergeDriver(root) + rollbackPairingMergeDriver = installPairingMergeDriver(root, worktreeConfigPath) git(['config', '--worktree', 'core.hooksPath', hooksPath], root) pathChanged = worktreePath !== hooksPath const installedEntry = effectiveConfigEntry(root, 'core.hooksPath') @@ -697,6 +793,7 @@ async function main() { runLefthook(root, lefthook) updateOwnershipMarker(ownedHooksDirectory.markerPath, hooksPath) } catch (error) { + const rollbackErrors = [] if (pathChanged) { try { if (worktreePath === undefined) { @@ -705,13 +802,21 @@ async function main() { git(['config', '--worktree', 'core.hooksPath', worktreePath], root) } } catch (rollbackError) { - throw new AggregateError( - [error, rollbackError], - `Lefthook installation failed: ${String(error)}; ` - + `worktree hook rollback also failed: ${String(rollbackError)}`, - ) + rollbackErrors.push(rollbackError) } } + try { + rollbackPairingMergeDriver() + } catch (rollbackError) { + rollbackErrors.push(rollbackError) + } + if (rollbackErrors.length > 0) { + throw new AggregateError( + [error, ...rollbackErrors], + `Lefthook installation failed: ${String(error)}; ` + + `worktree integration rollback also failed: ${rollbackErrors.map(String).join('; ')}`, + ) + } throw error } } catch (error) { diff --git a/scripts/install-lefthook.spec.ts b/scripts/install-lefthook.spec.ts index ca74b91404..4d5b585213 100644 --- a/scripts/install-lefthook.spec.ts +++ b/scripts/install-lefthook.spec.ts @@ -18,6 +18,9 @@ import { fileURLToPath } from 'node:url' import { afterEach, describe, expect, it } from 'vitest' const installer = fileURLToPath(new URL('./install-lefthook.mjs', import.meta.url)) +const pairingMergeDriver = 'scripts/merge-translation-pairing-driver.sh %O %A %B %P' +const scriptsDirectory = fileURLToPath(new URL('.', import.meta.url)) +const tsxPackageDirectory = dirname(fileURLToPath(import.meta.resolve('tsx/package.json'))) const fixtures: string[] = [] // Multi-worktree cases spawn several Git and Node subprocesses; coverage concurrency can // legitimately exceed Vitest's default deadline without changing the installer behavior. @@ -95,7 +98,7 @@ if (!shouldFail) { const binary = join(root, 'node_modules', '.bin', process.platform === 'win32' ? 'lefthook.cmd' : 'lefthook') const config = readFileSync(join(root, 'lefthook.yml'), 'utf8').trim() const hook = \`#!/bin/sh\\n# root=\${root}\\n# binary=\${binary}\\n# config=\${config}\\nexit 0\\n\` - for (const name of ['pre-commit', 'pre-push']) writeFileSync(join(hooksPath, name), hook, { mode: 0o755 }) + for (const name of ['pre-commit', 'pre-merge-commit', 'pre-push']) writeFileSync(join(hooksPath, name), hook, { mode: 0o755 }) } if (existsSync(running)) unlinkSync(running) if (process.env.DSH_TEST_LEFTHOOK_BREAK_WORKTREE_CONFIG === '1') { @@ -122,6 +125,12 @@ function installFakeLefthook(root: string): void { chmodSync(shim, 0o755) } +function installPairingProbeFixture(root: string): void { + const linkType = process.platform === 'win32' ? 'junction' : 'dir' + symlinkSync(scriptsDirectory, join(root, 'scripts'), linkType) + symlinkSync(tsxPackageDirectory, join(root, 'node_modules/tsx'), linkType) +} + function createFixture(names: { main?: string; linked?: string } = {}): Fixture { const container = mkdtempSync(join(tmpdir(), 'dsh-lefthook-')) fixtures.push(container) @@ -151,6 +160,8 @@ function createFixture(names: { main?: string; linked?: string } = {}): Fixture write(join(linked, 'lefthook.yml'), 'linked-worktree-config\n') installFakeLefthook(main) installFakeLefthook(linked) + installPairingProbeFixture(main) + installPairingProbeFixture(linked) return fixture } @@ -222,6 +233,9 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => { expect(git(fixture, fixture.main, ['config', '--get', 'core.repositoryFormatVersion'])).toBe('0') expect(existsSync(hooksPath(fixture, fixture.main))).toBe(false) expect(existsSync(join(common, 'config.worktree'))).toBe(false) + expect(gitResult(fixture, fixture.main, [ + 'config', '--get', 'merge.dsh-translation-pairing.driver', + ]).status).toBe(1) }) } @@ -241,6 +255,12 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => { expect(mainHooks).not.toBe(linkedHooks) expect(git(fixture, fixture.main, ['config', '--worktree', '--get', 'core.hooksPath'])).toBe(mainHooks) expect(git(fixture, fixture.linked, ['config', '--worktree', '--get', 'core.hooksPath'])).toBe(linkedHooks) + expect(git(fixture, fixture.main, [ + 'config', '--worktree', '--get', 'merge.dsh-translation-pairing.driver', + ])).toBe(pairingMergeDriver) + expect(git(fixture, fixture.linked, [ + 'config', '--worktree', '--get', 'merge.dsh-translation-pairing.driver', + ])).toBe(pairingMergeDriver) const mainHook = readFileSync(join(mainHooks, 'pre-commit'), 'utf8') const linkedHook = readFileSync(join(linkedHooks, 'pre-commit'), 'utf8') @@ -252,6 +272,8 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => { expect(linkedHook).toContain(`# root=${canonicalLinked}`) expect(linkedHook).toContain('# config=linked-worktree-config') expect(linkedHook).not.toContain(canonicalMain) + expect(existsSync(join(mainHooks, 'pre-merge-commit'))).toBe(true) + expect(existsSync(join(linkedHooks, 'pre-merge-commit'))).toBe(true) expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy hook\n') const commonConfig = join(common, 'config') @@ -275,6 +297,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => { git(fixture, fixture.main, ['worktree', 'add', '-b', 'late-linked', lateLinked]) write(join(lateLinked, 'lefthook.yml'), 'late-linked-worktree-config\n') installFakeLefthook(lateLinked) + installPairingProbeFixture(lateLinked) expect(git(fixture, lateLinked, ['config', '--worktree', '--get', 'core.hooksPath'])).toBe(mainHooks) const linkedInstall = await runInstaller(fixture, lateLinked) @@ -677,9 +700,50 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => { expect(result.stderr).toContain('command-scoped core.hooksPath') expect(readFileSync(sentinel, 'utf8')).toBe('#!/bin/sh\n# command-scope sentinel\n') expect(gitResult(fixture, fixture.main, ['config', '--get', 'core.hooksPath']).status).toBe(1) + expect(gitResult(fixture, fixture.main, [ + 'config', '--get', 'merge.dsh-translation-pairing.driver', + ]).status).toBe(1) expect(existsSync(hooksPath(fixture, fixture.main))).toBe(false) }) + it('never replaces a custom worktree pairing merge driver', async () => { + const fixture = createFixture() + const commonConfig = join(commonDirectory(fixture), 'config') + git(fixture, fixture.main, ['config', '--file', commonConfig, 'core.repositoryFormatVersion', '1']) + git(fixture, fixture.main, ['config', '--file', commonConfig, 'extensions.worktreeConfig', 'true']) + git(fixture, fixture.main, [ + 'config', '--worktree', 'merge.dsh-translation-pairing.driver', 'custom-driver %A', + ]) + + const result = await runInstaller(fixture, fixture.main) + + expect(result.status).toBe(1) + expect(result.stderr).toContain('refusing to replace worktree merge.dsh-translation-pairing.driver') + expect(git(fixture, fixture.main, [ + 'config', '--worktree', '--get', 'merge.dsh-translation-pairing.driver', + ])).toBe('custom-driver %A') + expect(gitResult(fixture, fixture.main, ['config', '--get', 'core.hooksPath']).status).toBe(1) + }) + + it('never masks an inherited custom pairing merge driver', async () => { + const fixture = createFixture() + git(fixture, fixture.main, [ + 'config', '--local', 'merge.dsh-translation-pairing.driver', 'inherited-driver %A', + ]) + + const result = await runInstaller(fixture, fixture.main) + + expect(result.status).toBe(1) + expect(result.stderr).toContain('refusing to mask inherited merge.dsh-translation-pairing.driver') + expect(git(fixture, fixture.main, [ + 'config', '--local', '--get', 'merge.dsh-translation-pairing.driver', + ])).toBe('inherited-driver %A') + expect(gitResult(fixture, fixture.main, [ + 'config', '--worktree', '--get', 'merge.dsh-translation-pairing.driver', + ]).status).toBe(1) + expect(gitResult(fixture, fixture.main, ['config', '--get', 'core.hooksPath']).status).toBe(1) + }) + it('does not pass unrelated command-scoped Git config to Lefthook', async () => { const fixture = createFixture() @@ -729,9 +793,29 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => { expect(result.stderr).toContain('exit status 77') expect(gitResult(fixture, fixture.main, ['config', '--worktree', '--get', 'core.hooksPath']).status).toBe(1) expect(gitResult(fixture, fixture.main, ['config', '--get', 'core.hooksPath']).status).toBe(1) + expect(gitResult(fixture, fixture.main, [ + 'config', '--worktree', '--get', 'merge.dsh-translation-pairing.name', + ]).status).toBe(1) + expect(gitResult(fixture, fixture.main, [ + 'config', '--worktree', '--get', 'merge.dsh-translation-pairing.driver', + ]).status).toBe(1) expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy pre-push\n') }) + it('does not publish worktree integration when the pairing driver probe fails', async () => { + const fixture = createFixture() + rmSync(join(fixture.main, 'node_modules/tsx'), { recursive: true, force: true }) + + const result = await runInstaller(fixture, fixture.main) + + expect(result.status).toBe(1) + expect(result.stderr).toContain('merge-translation-pairing.ts --probe failed') + expect(gitResult(fixture, fixture.main, ['config', '--get', 'core.hooksPath']).status).toBe(1) + expect(gitResult(fixture, fixture.main, [ + 'config', '--get', 'merge.dsh-translation-pairing.driver', + ]).status).toBe(1) + }) + it('reports installation and hook-path rollback failures together', async () => { const fixture = createFixture() @@ -743,8 +827,9 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => { expect(result.status).toBe(1) expect(result.stderr).toContain('Lefthook installation failed') expect(result.stderr).toContain('exit status 77') - expect(result.stderr).toContain('worktree hook rollback also failed') + expect(result.stderr).toContain('worktree integration rollback also failed') expect(result.stderr).toContain('git config --worktree --unset-all core.hooksPath failed') + expect(result.stderr).toContain('git config --worktree --unset-all merge.dsh-translation-pairing.driver failed') }) it('refuses an unowned directory at the reserved worktree hook path', async () => { diff --git a/scripts/merge-translation-pairing-driver.sh b/scripts/merge-translation-pairing-driver.sh new file mode 100755 index 0000000000..c1fffaff20 --- /dev/null +++ b/scripts/merge-translation-pairing-driver.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +if [ "$#" -ne 4 ]; then + echo 'merge-translation-pairing: expected ' >&2 + exit 129 +fi + +ancestor_path=$1 +current_path=$2 +other_path=$3 +meta_path=$4 +driver_directory=$(CDPATH= cd -P "$(dirname "$0")" && pwd) || exit 129 +driver_path=$driver_directory/merge-translation-pairing.ts + +if command -v node >/dev/null 2>&1 \ + && node --import tsx/esm "$driver_path" --probe >/dev/null 2>&1; then + exec node --import tsx/esm "$driver_path" \ + "$ancestor_path" "$current_path" "$other_path" "$meta_path" +fi + +echo "merge-translation-pairing: runtime is unavailable; leaving an ordinary text conflict in $meta_path" >&2 +git merge-file \ + -L "$meta_path:current" \ + -L "$meta_path:ancestor" \ + -L "$meta_path:other" \ + -- "$current_path" "$ancestor_path" "$other_path" +fallback_status=$? +echo 'merge-translation-pairing: restore Node dependencies, then rerun the merge or `pnpm run resolve-translation-pairing-conflicts`; use `git merge --abort` to cancel' >&2 + +# A clean text merge is still unverified pairing metadata, so the driver must +# leave Git's index stages unresolved until the repository-aware resolver runs. +if [ "$fallback_status" -gt 127 ]; then + exit "$fallback_status" +fi +exit 1 diff --git a/scripts/merge-translation-pairing.ts b/scripts/merge-translation-pairing.ts new file mode 100644 index 0000000000..dda2621a36 --- /dev/null +++ b/scripts/merge-translation-pairing.ts @@ -0,0 +1,51 @@ +/** Git merge-driver and explicit conflict-resolver entrypoint for pairing records. */ + +import { execFileSync } from 'node:child_process' +import { readFileSync, writeFileSync } from 'node:fs' +import { + mergeTranslationPairingRecords, + resolveTranslationPairingConflicts, +} from './translation-pairing-merge.ts' + +const args = process.argv.slice(2) + +try { + if (args[0] === '--probe') { + if (args.length !== 1) throw new Error('--probe takes no other arguments') + } else { + const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim() + if (args[0] === '--resolve') { + if (args.length !== 1) throw new Error('--resolve takes no paths; it inspects the unmerged index') + const resolved = resolveTranslationPairingConflicts(root) + if (resolved.length === 0) { + console.log('merge-translation-pairing: no unresolved pairing records') + } else { + for (const path of resolved) console.log(`merge-translation-pairing: resolved ${path}`) + } + } else { + if (args.length !== 4) { + throw new Error('merge-driver mode requires ') + } + const [ancestorPath, currentPath, otherPath, metaPath] = args + if (ancestorPath === undefined || currentPath === undefined || otherPath === undefined || metaPath === undefined) { + throw new Error('merge-driver arguments are incomplete') + } + const result = mergeTranslationPairingRecords( + root, + metaPath, + readFileSync(ancestorPath, 'utf8'), + readFileSync(currentPath, 'utf8'), + readFileSync(otherPath, 'utf8'), + ) + writeFileSync(currentPath, result.record) + } + } +} catch (error) { + console.error(`merge-translation-pairing: ${error instanceof Error ? error.message : String(error)}`) + console.error( + 'merge-translation-pairing: resolve owner conflicts, then confirm the pair with ' + + '`pnpm run verify-translation-pairing --write `; rerun ' + + '`pnpm run resolve-translation-pairing-conflicts` for other safe records', + ) + process.exitCode = 1 +} diff --git a/scripts/project-doc-site.spec.ts b/scripts/project-doc-site.spec.ts index 89e417558c..5d8f68f4cd 100644 --- a/scripts/project-doc-site.spec.ts +++ b/scripts/project-doc-site.spec.ts @@ -1,9 +1,9 @@ /** Tests for the documentation website projection adapter. */ import { execFileSync } from 'node:child_process' -import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { join, resolve } from 'node:path' +import { basename, join, resolve } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { docsPages, type DocsPage } from '../website/docs.ts' import { @@ -269,28 +269,41 @@ describe('docsPages locale routes', () => { } }) - it('projects translated core-data pages while retaining explicit English fallbacks', () => { + it('indexes every subsystem page in both sides of the folder README', () => { + const pages = globSync(join(repositoryRoot, 'docs/subsystems/*.md')) + .map(page => basename(page)) + .filter(page => !page.endsWith('.zh.md') && page !== 'README.md') + .sort() + expect(pages.length).toBeGreaterThan(0) + for (const readme of ['README.md', 'README.zh.md']) { + const rows = readFileSync(join(repositoryRoot, 'docs/subsystems', readme), 'utf8') + const missing = pages.filter(page => !rows.includes(`| [${page}](${page}) |`)) + expect(missing, `${readme} must carry one table row per subsystem page`).toEqual([]) + } + }) + + it('projects translated subsystem pages while retaining explicit English fallbacks', () => { const rootPages = docsPages.filter(page => ( - page.locale === 'root' && page.route.startsWith('reference/core-data-structures/') + page.locale === 'root' && page.route.startsWith('reference/subsystems/') )) 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(39) expect(translated.every(page => page.source.endsWith('.zh.md'))).toBe(true) expect(fallbacks.map(page => page.source).sort()).toEqual([ - 'docs/core-data-structures/commands.md', - 'docs/core-data-structures/goal.md', - 'docs/core-data-structures/pty.md', + 'docs/subsystems/commands.md', + 'docs/subsystems/goal.md', + 'docs/subsystems/pty.md', ]) }) 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/run-gates.ts b/scripts/run-gates.ts index 74a09a874e..d99889d28b 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -312,7 +312,7 @@ function nodeCompatSmokeGates(options: { cliSmoke?: boolean } = {}): Gate[] { pnpmExec('jsonl-zstd-smoke', [ 'vitest', 'run', - 'packages/session-persistence/session-persistence-jsonl/tests/zstd.compat.spec.ts', + 'packages/session/session-persistence-jsonl/tests/zstd.compat.spec.ts', ], { label: 'JSONL Zstandard smoke' }), pnpmExec('dsh-source-launch-smoke', [ 'vitest', @@ -619,7 +619,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate { 'apps/cli/tests/built-bin.e2e.ts', 'packages/examples/acp-demo/tests/built-bin.e2e.ts', 'packages/host/directory-picker-native/tests/built-worker.e2e.ts', - 'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts', + 'packages/scaffold/server/tests/built-scope-carrier.e2e.ts', 'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts', 'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts', 'packages/api/remotes/tests/built-lib.e2e.ts', diff --git a/scripts/snapshots/translation-prompt-v4/request-response.expected.json b/scripts/snapshots/translation-prompt-v4/request-response.expected.json index baa648925d..137315cf39 100644 --- a/scripts/snapshots/translation-prompt-v4/request-response.expected.json +++ b/scripts/snapshots/translation-prompt-v4/request-response.expected.json @@ -8,27 +8,27 @@ }, { "role": "user", - "content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK.\n\nIt uses an architecture where **everything is a plugin**.\n\n## Internal testing notice\n\nDeepSeek Harness is under internal testing. Features and interfaces may change.\n\nThe internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group.\n\n## Install\n\nClone the repository, then run the installer:\n\n```sh\ngit clone \ncd deepseek-harness\nscripts/install.sh\n```\n\nThe installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, builds the required repository artifacts, and launches the Web UI.\n\nThe default active checkout is `~/.dsh/source/current`, and the launcher is linked into `~/.local/bin`. Re-run the installer to update. [`scripts/install.sh`](scripts/install.sh) owns alternate locations, update mechanics, and recovery options.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nFor the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\nThe path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.\n\n### Profiles\n\n`dsh` boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nThe [CLI contract](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### Automation and SDKs\n\nFrom a source checkout with `DEEPSEEK_API_KEY` in the environment or its root `.env`, start the ACP automation server:\n\n```sh\npnpm run demo:acp\n```\n\nThe [Python SDK](python/README.md) drives a bundled JSON-RPC runtime. The [examples](examples/README.md) cover the runnable headless, ACP, JSON-RPC, Code Mode, and self-referential compositions.\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell and persistent PTY execution; reusable skills; task tracking, goals, plans, todos, and background tasks; subagents and workflows; sandboxing and approvals; settings and credentials; persistent, resumable, forkable, and queryable sessions; LSP and web access; context compaction; and telemetry. Each composition selects the subset appropriate to its surface. The Web UI includes Plan Mode.\n\n- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design.\n- **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log).\n- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode).\n- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/cordis/tool-cordis/README.md).\n\n## Community\n\nFollow DeepSeek Harness on Twitter for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\nDeepSeek Harness is currently in internal testing.\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n" + "content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK.\n\nIt uses an architecture where **everything is a plugin**.\n\n## Internal testing notice\n\nDeepSeek Harness is under internal testing. Features and interfaces may change.\n\nThe internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group.\n\n## Install\n\nClone the repository, then run the installer:\n\n```sh\ngit clone \ncd deepseek-harness\nscripts/install.sh\n```\n\nThe installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, builds the required repository artifacts, and launches the Web UI.\n\nThe default active checkout is `~/.dsh/source/current`, and the launcher is linked into `~/.local/bin`. Re-run the installer to update. [`scripts/install.sh`](scripts/install.sh) owns alternate locations, update mechanics, and recovery options.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nFor the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\nThe path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.\n\n### Profiles\n\n`dsh` boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nThe [CLI contract](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### Automation and SDKs\n\nFrom a source checkout with `DEEPSEEK_API_KEY` in the environment or its root `.env`, start the ACP automation server:\n\n```sh\npnpm run demo:acp\n```\n\nThe [Python SDK](python/README.md) drives a bundled JSON-RPC runtime. The [examples](examples/README.md) cover the runnable headless, ACP, JSON-RPC, Code Mode, and self-referential compositions.\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell and persistent PTY execution; reusable skills; task tracking, goals, plans, todos, and background tasks; subagents and workflows; sandboxing and approvals; settings and credentials; persistent, resumable, forkable, and queryable sessions; LSP and web access; context compaction; and telemetry. Each composition selects the subset appropriate to its surface. The Web UI includes Plan Mode.\n\n- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design.\n- **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log).\n- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode).\n- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/self-modification/tool-cordis/README.md).\n\n## Community\n\nFollow DeepSeek Harness on Twitter for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\nDeepSeek Harness is currently in internal testing.\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n" }, { "role": "assistant", - "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(编程智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。\n\n## 安装\n\n克隆仓库,然后运行安装器:\n\n```sh\ngit clone \ncd deepseek-harness\nscripts/install.sh\n```\n\n安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。\n\n默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\n上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### Profile\n\n`dsh` 启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nprofile 布局、层语义与配置输出命令详见 [CLI(命令行界面)契约](apps/cli/README.md#profiles)。\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### 自动化与 SDK\n\n在源码检出中通过环境变量或根目录 `.env` 设置 `DEEPSEEK_API_KEY`,然后启动 ACP(Agent Client Protocol)自动化服务器:\n\n```sh\npnpm run demo:acp\n```\n\n[Python SDK](python/README.md) 驱动随附的 JSON-RPC 运行时。[示例](examples/README.md)涵盖可运行的 headless、ACP、JSON-RPC、Code Mode 和自指组合。\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 和持久 PTY 执行、可复用 skill(技能)、任务跟踪、目标、计划、待办事项与后台任务、subagent 与工作流、沙箱与审批、设置与凭据、可持久化、恢复、fork 与查询的会话、LSP 与 Web 访问、上下文压缩(context compaction),以及遥测。每个组合只选用适合其使用方式的能力子集。Web UI 包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均可组合为 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **运行可重建。** 凡是模型可见的内容,都会记录在权威会话流中;持久化、恢复/fork/查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/cordis/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

\n \"DeepSeek\n

\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\nDeepSeek Harness 目前处于内测阶段。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n" + "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(编程智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。\n\n## 安装\n\n克隆仓库,然后运行安装器:\n\n```sh\ngit clone \ncd deepseek-harness\nscripts/install.sh\n```\n\n安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。\n\n默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\n上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### Profile\n\n`dsh` 启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nprofile 布局、层语义与配置输出命令详见 [CLI(命令行界面)契约](apps/cli/README.md#profiles)。\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### 自动化与 SDK\n\n在源码检出中通过环境变量或根目录 `.env` 设置 `DEEPSEEK_API_KEY`,然后启动 ACP(Agent Client Protocol)自动化服务器:\n\n```sh\npnpm run demo:acp\n```\n\n[Python SDK](python/README.md) 驱动随附的 JSON-RPC 运行时。[示例](examples/README.md)涵盖可运行的 headless、ACP、JSON-RPC、Code Mode 和自指组合。\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 和持久 PTY 执行、可复用 skill(技能)、任务跟踪、目标、计划、待办事项与后台任务、subagent 与工作流、沙箱与审批、设置与凭据、可持久化、恢复、fork 与查询的会话、LSP 与 Web 访问、上下文压缩(context compaction),以及遥测。每个组合只选用适合其使用方式的能力子集。Web UI 包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均可组合为 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **运行可重建。** 凡是模型可见的内容,都会记录在权威会话流中;持久化、恢复/fork/查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/self-modification/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

\n \"DeepSeek\n

\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\nDeepSeek Harness 目前处于内测阶段。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n" }, { "role": "user", - "content": "# Development guide\n\nEnglish | [中文](development.zh.md)\n\nThe setup tutorial takes a new contributor from prerequisites to a checked checkout. The contributor reference that follows covers repository layout, daily workflow, and CI shape. Design rationale and implementation details belong to the linked Agent Notes and scripts.\n\n## Setup tutorial\n\n### Prerequisites\n\n- Node.js supports 22.19+ and 24+. CI covers 22.19, 24, and 26; see the [Node engine floor Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md).\n- Corepack-enabled pnpm. The repo pins `pnpm@11.7.0` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack.\n- Git 2.26 or newer; hook setup enables Git's worktree-specific configuration extension.\n- Optional: a DeepSeek API key for the Web, headless, and ACP automation demos and real-API e2e tests.\n\n### First-time setup\n\nInstall dependencies from the repo root:\n\n```sh\npnpm install\n```\n\nThe install also configures worktree-local lefthook hooks through `scripts/install-lefthook.mjs`. The [worktree-local hooks Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) owns the safety and migration contract.\n\nIf hooks are missing because dependencies were restored from cache or `postinstall` was skipped, install them manually:\n\n```sh\nnode scripts/install-lefthook.mjs\n```\n\nIf the wrapper rejects existing Git configuration or reports a stale lock, follow its diagnostic and the linked Agent Note rather than editing worktree metadata speculatively. After moving a checkout, rerun the wrapper to regenerate the owned path.\n\nRun typecheck once after a fresh clone:\n\n```sh\npnpm run typecheck\n```\n\nSetup is complete when `pnpm run typecheck` exits successfully.\n\n## Contributor reference\n\n### TypeScript project layout\n\nThe repository uses isolated Host and Client aggregates. An ordinary package is registered in exactly one aggregate: Host packages in `tsconfig.host.json` and Client packages in `tsconfig.client.json`.\n\n| File | Role | Forms a program? |\n|---|---|---|\n| `tsconfig.json` | Solution root: `extends` base, `files: []`, and references to the two aggregates. It is the tsserver discovery entry and the entry for explicitly running the complete Project Reference graph; through the inherited `paths`, it is also the resolution config for tsx running `examples/` and `scripts/`. | No |\n| `tsconfig.host.json` | Host aggregate: Host packages, examples, tests, scripts, website, and the exceptional Host project of `api/remotes`. | Yes |\n| `tsconfig.client.json` | Client aggregate: `packages/client/*` packages and their tests, `apps/web`, and the exceptional Client project of `api/remotes`. | Yes |\n| `tsconfig.base.json` | Shared compilerOptions and the source `paths` map. Also the resolution facade the vitest configs point vite-tsconfig-paths at: it has no `include`, so its `paths` apply to every importer. | No |\n| `tsconfig.base.client.json` | Browser compiler shape (`jsx`, DOM libs, `types: []`) extended by the Client aggregate and every `packages/client/*` package. | No |\n\nHost and Client stay two aggregate programs because both sides declaration-merge the cordis `Context` interface under the same keys with different services; one program seeing both merges reports a collision. The collision exists only inside a `ts.Program` — module resolution never triggers it — which is why the solution may reference both aggregates and one paths facade may span both sides. Three disciplines follow:\n\n- `tsconfig.base.json` never gains `include` or `files`: they would leak into every extending package project and narrow the facade's match-all scope.\n- A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges.\n- A new package is registered in exactly one aggregate. Having both a Node loader entry and a browser entry is not a reason to split a package; an ordinary Client plugin produces both runtime artifacts during the Client build phase.\n\n`api/remotes` is the repository's only package with split Host and Client tsconfigs. Its Host entry must participate in the Host TypeRT graph, while its Client entry imports `/remote` declarations that Host tsdown must generate first. The package-root `tsconfig.json` is therefore only a solution, and the two aggregates and direct consumers reference `tsconfig.host.json` or `tsconfig.client.json` respectively. The workspace `constraints` gate walks the reachable Project Reference graph and checks each referencing project's own compiler face: a single-config target remains valid from either face, while a split target must name the matching leaf rather than its solution root or opposite leaf. Do not copy this structure to other packages; see the [`api-remotes` README](../packages/api/remotes/README.md) for the complete boundary.\n\nThe root build follows the generated dependency order:\n\n```sh\ntsc -b tsconfig.host.json\ntsdown --env.DSH_BUILD_FACE host\ntsc -b tsconfig.client.json\ntsdown --env.DSH_BUILD_FACE client\npnpm run build:web\n```\n\nBoth tsdown passes use the same complete workspace match. They neither scan build artifacts to discover Client packages nor maintain a Host/Client package filter list. Package-local tsdown configs select entries for the current phase through `DSH_BUILD_FACE`: an ordinary Client plugin produces both its Node loader and browser bundle during the Client phase; `api-remotes` uses `hostPhase: true` to produce its Host entry early and only its browser bundle during the Client phase. Tsdown consumes only the JavaScript emitted to `lib/types` by the preceding tsc phase.\n\nTypeRT runs only during Host tsdown, seeded by `tsconfig.host.json`. It analyzes Host types and generates both Host reflection artifacts and the Host-for-Client Remote projection; Client tsdown does not start TypeRT. Consequently, `pnpm run typecheck` runs the complete Host lib phase before Client tsc, while `pnpm run build` continues through Client tsdown and the Web build. The [API Remotes generated-contract build note](../.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.md) records this ordering decision.\n\nStatic analysis and tests resolve workspace imports through the base `paths` map to `src` and must pass on a clean tree; gates that consume built `lib/` output declare that dependency explicitly. Generated Host-for-Client Remote declarations are the deliberate exception: the public `typecheck`, `lint`, and `doc-typecheck` commands generate them first, while internal `*:contracts-ready` scripts assume that an invoking public command or scheduler gate already owns an explicit dependency on the TypeRT contract pass or the complete build. See the [solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md) for the two-aggregate topology, the [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md) for tsc-first emit ownership, and the [TypeRT Remote note](../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md) for the gate-preparation contract.\n\nBusiness services declare callable methods on the Host with `@Remote` or `@RemoteScope`; the Host build generates Host-for-Client types and runtime contributions, and the Client's `api-remotes` composition loads those contributions under `ctx.remote` and scoped `agentCtx.remote` namespaces. See [API Gateway](api-gateway.md) for the generated artifacts on both sides, their assembly relationships, the SRC development fallback, and the Web build order.\n\nIf a relevant local check consumes built package output, build once first:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files, and `verify-node-next-types`, which validates built declarations against a temporary NodeNext consumer. A fresh worktree has no bundled JS or declarations until `pnpm run build` runs; ordinary commits and pushes do not require that build unless their selected checks consume it.\n\n### Environment variables\n\nThe real DeepSeek adapter and key-backed agent demos read credentials from the environment or from a gitignored `.env` at the repo root:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` is optional and defaults to the public API. Never commit real credentials. The real-API e2e suites self-skip when `DEEPSEEK_API_KEY` is not set.\n\n### Git hooks\n\nlefthook is configured in `lefthook.yml` as a fast local checkpoint:\n\n- `pre-commit` applies formatting-only ESLint fixes, validates the staged files with the project-free `.oxlintrc.staged.json` profile and applies Oxlint's native fixes, regenerates `THIRD_PARTY_NOTICES.md` when a staged file is one of its inputs, checks the staged diff for whitespace errors, and runs the vendor manifest guard.\n- `pre-push` runs `pnpm run typecheck`, which completes the Host lib phase, including generated TypeRT contracts, before the Client TypeScript check.\n\nThe vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code.\n\nThe hooks intentionally do not run tests, snapshots, documentation checks, builds, or hygiene. Contributors run the [checks relevant to the changed behavior](../AGENTS.md#run-relevant-checks-locally) once; CI owns exhaustive coverage, built-artifact smokes, and the Node 22.19, 24, and 26 compatibility matrix.\n\nContributors can opt into the comprehensive local gate set with `pnpm run check:all`. The command is independent of both Git hooks and is not an agent instruction.\n\n### CI gates\n\nThe keyless [CI workflow](../.github/workflows/ci.yml) groups independent gates into broad lanes and runs a smaller compatibility signal across supported Node versions. Artifact consumers wait for one build within their lane. The separate real-API workflow runs `pnpm run test:e2e` with its configured worker bound. See [scripts/run-gates.ts](../scripts/run-gates.ts) and the workflow files for the current gate and job inventory.\n\n### Daily commands\n\nThe root [contributor instructions](../AGENTS.md#commands) summarize common commands, while [`package.json`](../package.json) and [scripts/run-gates.ts](../scripts/run-gates.ts) own the current script and gate inventories. Select the smallest checks that cover the changed surface. Documentation changes use `pnpm run doc-sync`; package-public behavior changes also update the owning README or JSDoc, and built-artifact checks require `pnpm run build` first.\n\n### Demos\n\nThe one-shot Headless coding agent needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`:\n\n```sh\npnpm run demo:headless \"summarize this workspace\"\n```\n\nThe self-referential cordis demo can inspect and modify its live plugin runtime and needs the same credentials (`web` by default, or `acp`):\n\n```sh\npnpm run demo:cordis\n```\n\nThe ACP automation server exposes fresh agent sessions over JSON-RPC stdio and also needs `DEEPSEEK_API_KEY`:\n\n```sh\npnpm run demo:acp\n```\n\n### TODO markers\n\nUse one of three comment tags to flag known issues in the code, ordered by urgency:\n\n- `FIXME` — an issue that should block a new release. A release should not ship with an open `FIXME` unless reviewers explicitly agree the change can be merged anyway.\n- `TODO` — an issue that should be fixed soon, once we have the resources.\n- `XXX` — an issue that we may fix someday; lowest priority, no commitment.\n\nPick the tag that matches the urgency so anyone scanning the code can tell a release blocker from a someday-maybe.\n\n### Documenting types verbatim (`ts type-equiv`)\n\nThe [core data structures](core-data-structures/core.md) docs paste source-equivalent declarations together with their original JSDoc so a reader sees the exact shape and source contract. To keep a paste from drifting when source changes, fence it as ` ```ts type-equiv ` (instead of ` ```ts `) and register it in `scripts/type-equiv.manifest.json` with the source file and symbol it mirrors:\n\n```json\n{ \"doc\": \"docs/core-data-structures/session.md\", \"symbol\": \"SessionEvent\", \"source\": \"packages/core/session/src/types.ts\" }\n```\n\n`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `\"projection\": \"public-api\"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact shape. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change.\n" + "content": "# Development guide\n\nEnglish | [中文](development.zh.md)\n\nThe setup tutorial takes a new contributor from prerequisites to a checked checkout. The contributor reference that follows covers repository layout, daily workflow, and CI shape. Design rationale and implementation details belong to the linked Agent Notes and scripts.\n\n## Setup tutorial\n\n### Prerequisites\n\n- Node.js supports 22.19+ and 24+. CI covers 22.19, 24, and 26; see the [Node engine floor Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md).\n- Corepack-enabled pnpm. The repo pins `pnpm@11.7.0` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack.\n- Git 2.26 or newer; hook setup enables Git's worktree-specific configuration extension.\n- Optional: a DeepSeek API key for the Web, headless, and ACP automation demos and real-API e2e tests.\n\n### First-time setup\n\nInstall dependencies from the repo root:\n\n```sh\npnpm install\n```\n\nThe install also configures worktree-local Lefthook hooks and the `dsh-translation-pairing` Git merge driver through `scripts/install-lefthook.mjs`. The [worktree-local hooks Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) owns the hook-path safety contract; the [automatic pairing merges Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) owns the merge driver.\n\nIf either integration is missing because dependencies were restored from cache or `postinstall` was skipped, install them manually:\n\n```sh\nnode scripts/install-lefthook.mjs\n```\n\nIf the wrapper rejects existing Git configuration or reports a stale lock, follow its diagnostic and the linked Agent Note rather than editing worktree metadata speculatively. After moving a checkout, rerun the wrapper to regenerate the owned path.\n\nRun typecheck once after a fresh clone:\n\n```sh\npnpm run typecheck\n```\n\nSetup is complete when `pnpm run typecheck` exits successfully.\n\n## Contributor reference\n\n### TypeScript project layout\n\nThe repository uses isolated Host and Client aggregates. An ordinary package is registered in exactly one aggregate: Host packages in `tsconfig.host.json` and Client packages in `tsconfig.client.json`.\n\n| File | Role | Forms a program? |\n|---|---|---|\n| `tsconfig.json` | Solution root: `extends` base, `files: []`, and references to the two aggregates. It is the tsserver discovery entry and the entry for explicitly running the complete Project Reference graph; through the inherited `paths`, it is also the resolution config for tsx running `examples/` and `scripts/`. | No |\n| `tsconfig.host.json` | Host aggregate: Host packages, examples, tests, scripts, website, and the exceptional Host project of `api/remotes`. | Yes |\n| `tsconfig.client.json` | Client aggregate: `packages/client/*` packages and their tests, `apps/web`, and the exceptional Client project of `api/remotes`. | Yes |\n| `tsconfig.base.json` | Shared compilerOptions and the source `paths` map. Also the resolution facade the vitest configs point vite-tsconfig-paths at: it has no `include`, so its `paths` apply to every importer. | No |\n| `tsconfig.base.client.json` | Browser compiler shape (`jsx`, DOM libs, `types: []`) extended by the Client aggregate and every `packages/client/*` package. | No |\n\nHost and Client stay two aggregate programs because both sides declaration-merge the cordis `Context` interface under the same keys with different services; one program seeing both merges reports a collision. The collision exists only inside a `ts.Program` — module resolution never triggers it — which is why the solution may reference both aggregates and one paths facade may span both sides. Three disciplines follow:\n\n- `tsconfig.base.json` never gains `include` or `files`: they would leak into every extending package project and narrow the facade's match-all scope.\n- A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges.\n- A new package is registered in exactly one aggregate. Having both a Node loader entry and a browser entry is not a reason to split a package; an ordinary Client plugin produces both runtime artifacts during the Client build phase.\n\n`api/remotes` is the repository's only package with split Host and Client tsconfigs. Its Host entry must participate in the Host TypeRT graph, while its Client entry imports `/remote` declarations that Host tsdown must generate first. The package-root `tsconfig.json` is therefore only a solution, and the two aggregates and direct consumers reference `tsconfig.host.json` or `tsconfig.client.json` respectively. The workspace `constraints` gate walks the reachable Project Reference graph and checks each referencing project's own compiler face: a single-config target remains valid from either face, while a split target must name the matching leaf rather than its solution root or opposite leaf. Do not copy this structure to other packages; see the [`api-remotes` README](../packages/api/remotes/README.md) for the complete boundary.\n\nThe root build follows the generated dependency order:\n\n```sh\ntsc -b tsconfig.host.json\ntsdown --env.DSH_BUILD_FACE host\ntsc -b tsconfig.client.json\ntsdown --env.DSH_BUILD_FACE client\npnpm run build:web\n```\n\nBoth tsdown passes use the same complete workspace match. They neither scan build artifacts to discover Client packages nor maintain a Host/Client package filter list. Package-local tsdown configs select entries for the current phase through `DSH_BUILD_FACE`: an ordinary Client plugin produces both its Node loader and browser bundle during the Client phase; `api-remotes` uses `hostPhase: true` to produce its Host entry early and only its browser bundle during the Client phase. Tsdown consumes only the JavaScript emitted to `lib/types` by the preceding tsc phase.\n\nTypeRT runs only during Host tsdown, seeded by `tsconfig.host.json`. It analyzes Host types and generates both Host reflection artifacts and the Host-for-Client Remote projection; Client tsdown does not start TypeRT. Consequently, `pnpm run typecheck` runs the complete Host lib phase before Client tsc, while `pnpm run build` continues through Client tsdown and the Web build. The [API Remotes generated-contract build note](../.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.md) records this ordering decision.\n\nStatic analysis and tests resolve workspace imports through the base `paths` map to `src` and must pass on a clean tree; gates that consume built `lib/` output declare that dependency explicitly. Generated Host-for-Client Remote declarations are the deliberate exception: the public `typecheck`, `lint`, and `doc-typecheck` commands generate them first, while internal `*:contracts-ready` scripts assume that an invoking public command or scheduler gate already owns an explicit dependency on the TypeRT contract pass or the complete build. See the [solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md) for the two-aggregate topology, the [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md) for tsc-first emit ownership, and the [TypeRT Remote note](../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md) for the gate-preparation contract.\n\nBusiness services declare callable methods on the Host with `@Remote` or `@RemoteScope`; the Host build generates Host-for-Client types and runtime contributions, and the Client's `api-remotes` composition loads those contributions under `ctx.remote` and scoped `agentCtx.remote` namespaces. See [API Gateway](api-gateway.md) for the generated artifacts on both sides, their assembly relationships, the SRC development fallback, and the Web build order.\n\nIf a relevant local check consumes built package output, build once first:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files, and `verify-node-next-types`, which validates built declarations against a temporary NodeNext consumer. A fresh worktree has no bundled JS or declarations until `pnpm run build` runs; ordinary commits and pushes do not require that build unless their selected checks consume it.\n\n### Environment variables\n\nThe real DeepSeek adapter and key-backed agent demos read credentials from the environment or from a gitignored `.env` at the repo root:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` is optional and defaults to the public API. Never commit real credentials. The real-API e2e suites self-skip when `DEEPSEEK_API_KEY` is not set.\n\n### Git integrations\n\nThe pairing merge driver derives a conflicted `.i18n.yaml` record from the confirmed ancestor, current, and other owner blobs when both language files use Git's default text strategy and merge cleanly. It fails closed on owner conflicts, non-text merge configuration, or invalid records; after an already-stopped merge, run `pnpm run resolve-translation-pairing-conflicts`, which stages every safe pairing record and exits unsuccessfully if other pairing conflicts still need manual work. See the [bilingual documentation contract](i18n/README.md#the-pairing-contract) for the exact boundary.\n\nThe installer probes the exact Node/tsx driver entrypoint before publishing its worktree configuration. If that runtime later becomes unavailable, the Node-independent launcher writes Git's ordinary text result, leaves the sidecar unresolved, and prints the recovery path; restore dependencies and run `pnpm run resolve-translation-pairing-conflicts`, or run `git merge --abort`. If `pre-merge-commit` rejects an otherwise clean merge, Git leaves the complete result staged without a commit; repair the failure and run `git commit`, or abort. The [automatic pairing merges Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md#failure-contract) owns the exact index and `MERGE_HEAD` states.\n\nlefthook is configured in `lefthook.yml` as a fast local checkpoint:\n\n- `pre-commit` verifies staged pairing records against the staged owner blobs, applies formatting-only ESLint fixes, validates the staged files with the project-free `.oxlintrc.staged.json` profile and applies Oxlint's native fixes, regenerates `THIRD_PARTY_NOTICES.md` when a staged file is one of its inputs, checks the staged diff for whitespace errors, and runs the vendor manifest guard.\n- `pre-merge-commit` performs the same index-backed pairing check before Git creates an automatic merge commit.\n- `pre-push` runs `pnpm run typecheck`, which completes the Host lib phase, including generated TypeRT contracts, before the Client TypeScript check.\n\nThe vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code.\n\nApart from the scoped staged-record verification, the hooks intentionally do not run tests, snapshots, documentation checks, builds, or hygiene. Contributors run the [checks relevant to the changed behavior](../AGENTS.md#run-relevant-checks-locally) once; CI owns exhaustive coverage, built-artifact smokes, and the Node 22.19, 24, and 26 compatibility matrix.\n\nContributors can opt into the comprehensive local gate set with `pnpm run check:all`. The command is independent of the Git hooks and is not an agent instruction.\n\n### CI gates\n\nThe keyless [CI workflow](../.github/workflows/ci.yml) groups independent gates into broad lanes and runs a smaller compatibility signal across supported Node versions. Artifact consumers wait for one build within their lane. The separate real-API workflow runs `pnpm run test:e2e` with its configured worker bound. See [scripts/run-gates.ts](../scripts/run-gates.ts) and the workflow files for the current gate and job inventory.\n\n### Daily commands\n\nThe root [contributor instructions](../AGENTS.md#commands) summarize common commands, while [`package.json`](../package.json) and [scripts/run-gates.ts](../scripts/run-gates.ts) own the current script and gate inventories. Select the smallest checks that cover the changed surface. Documentation changes use `pnpm run doc-sync`; package-public behavior changes also update the owning README or JSDoc, and built-artifact checks require `pnpm run build` first.\n\n### Demos\n\nThe one-shot Headless coding agent needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`:\n\n```sh\npnpm run demo:headless \"summarize this workspace\"\n```\n\nThe self-referential cordis demo can inspect and modify its live plugin runtime and needs the same credentials (`web` by default, or `acp`):\n\n```sh\npnpm run demo:cordis\n```\n\nThe ACP automation server exposes fresh agent sessions over JSON-RPC stdio and also needs `DEEPSEEK_API_KEY`:\n\n```sh\npnpm run demo:acp\n```\n\n### TODO markers\n\nUse one of three comment tags to flag known issues in the code, ordered by urgency:\n\n- `FIXME` — an issue that should block a new release. A release should not ship with an open `FIXME` unless reviewers explicitly agree the change can be merged anyway.\n- `TODO` — an issue that should be fixed soon, once we have the resources.\n- `XXX` — an issue that we may fix someday; lowest priority, no commitment.\n\nPick the tag that matches the urgency so anyone scanning the code can tell a release blocker from a someday-maybe.\n\n### Documenting types verbatim (`ts type-equiv`)\n\nThe [subsystems](subsystems/README.md) pages paste source-equivalent declarations together with their original JSDoc so a reader sees the exact shape and source contract. To keep a paste from drifting when source changes, fence it as ` ```ts type-equiv ` (instead of ` ```ts `) and register it in `scripts/type-equiv.manifest.json` with the source file and symbol it mirrors:\n\n```json\n{ \"doc\": \"docs/subsystems/session.md\", \"symbol\": \"SessionEvent\", \"source\": \"packages/core/session/src/types.ts\" }\n```\n\n`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `\"projection\": \"public-api\"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact shape. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change.\n" }, { "role": "assistant", - "content": "# 开发指南\n\n[English](development.md) | 中文\n\n搭建教程引导新贡献者从准备前置条件开始,直到检出通过检查。后面的贡献者参考介绍仓库布局、日常工作流和 CI 形态。设计依据与实现细节属于链接的 Agent Note 和脚本。\n\n## 搭建教程\n\n### 前置条件\n\n- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md)。\n- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。\n- Git 2.26 或更高版本;钩子设置会启用 Git 的 worktree 专属配置扩展。\n- 可选:一个 DeepSeek API key,用于 Web、headless 和 ACP(Agent Client Protocol)自动化 agent(智能体)演示以及真实 API 的 e2e 测试。\n\n### 首次搭建\n\n在仓库根目录安装依赖:\n\n```sh\npnpm install\n```\n\n安装过程还会通过 `scripts/install-lefthook.mjs` 配置 worktree 本地的 lefthook 钩子。其安全与迁移契约由 [worktree 本地钩子 Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) 负责。\n\n如果依赖是从缓存恢复或 `postinstall` 被跳过而导致缺少钩子,请手动安装:\n\n```sh\nnode scripts/install-lefthook.mjs\n```\n\n如果包装脚本拒绝现有 Git 配置或报告陈旧锁,请遵循其诊断和所链接的 Agent Note,不要凭猜测编辑 worktree 元数据。移动检出目录后,请重新运行包装脚本以重新生成自有路径。\n\n新克隆后请先运行一次类型检查:\n\n```sh\npnpm run typecheck\n```\n\n`pnpm run typecheck` 成功退出即表示搭建完成。\n\n## 贡献者参考\n\n### TypeScript 项目布局\n\n仓库使用相互隔离的 Host 与 Client aggregate。普通 package 只登记进其中一个 aggregate;Host 包进入 `tsconfig.host.json`,Client 包进入 `tsconfig.client.json`。\n\n| 文件 | 角色 | 是否构成 program? |\n|---|---|---|\n| `tsconfig.json` | solution 根:`extends` base、`files: []`、引用两个 aggregate。它是 tsserver 发现入口,也是显式执行整张 Project Reference 图时的入口;经继承的 `paths` 充当 tsx 运行 `examples/` 与 `scripts/` 时的解析配置。 | 否 |\n| `tsconfig.host.json` | Host aggregate:Host package、示例、测试、脚本和 website,以及 `api/remotes` 的 Host 特例 project。 | 是 |\n| `tsconfig.client.json` | Client aggregate:`packages/client/*` package 及其测试、`apps/web`,以及 `api/remotes` 的 Client 特例 project。 | 是 |\n| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 |\n| `tsconfig.base.client.json` | 浏览器编译形状(`jsx`、DOM lib、`types: []`),由 Client aggregate 和每个 `packages/client/*` package extends。 | 否 |\n\nHost 与 Client 保持两个 aggregate program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个 aggregate,一个 paths 门面也可以横跨两侧。由此推出三条纪律:\n\n- `tsconfig.base.json` 永不添加 `include` 或 `files`:它们会泄漏进每个 extends 它的包项目,并收窄门面的全匹配范围。\n- 构造全仓 `ts.Program` 的脚本显式种子 `tsconfig.host.json` 或 `tsconfig.client.json`——永不种子根 solution,因为把两个 aggregate 展平进一个 program 会撞上 `Context` 合并冲突。\n- 新 package 只登记进一个 aggregate。包同时具有 Node loader 入口和 browser 入口并不构成拆分理由;普通 Client plugin 的两份运行时产物都在 Client 构建阶段生成。\n\n`api/remotes` 是唯一拆分 Host/Client tsconfig 的仓库特例。它的 Host 入口必须进入 Host TypeRT 图,而 Client 入口导入 Host tsdown 才会生成的 `/remote` 声明,因此本包根 `tsconfig.json` 只作为 solution,两个 aggregate 和直接消费方分别引用 `tsconfig.host.json` 或 `tsconfig.client.json`。workspace `constraints` 门禁遍历可达的 Project Reference 图,并按各引用 project 自身的 compiler face 检查:只有单一配置的目标可由任一 face 引用,拆分配置的目标则必须引用匹配的 leaf,不得引用 solution 根或另一侧 leaf。不要把该结构推广到其他包;完整边界见 [`api-remotes` README](../packages/api/remotes/README.md)。\n\n根构建按生成依赖排序:\n\n```sh\ntsc -b tsconfig.host.json\ntsdown --env.DSH_BUILD_FACE host\ntsc -b tsconfig.client.json\ntsdown --env.DSH_BUILD_FACE client\npnpm run build:web\n```\n\n两次 tsdown 都使用同一组完整 workspace 匹配,不扫描构建产物来发现 Client package,也不维护 Host/Client package 过滤表。包内 tsdown 配置根据 `DSH_BUILD_FACE` 决定当前阶段的入口:普通 Client plugin 在 Client 阶段同时生成 Node loader 与 browser bundle;`api-remotes` 通过 `hostPhase: true` 提前生成 Host 入口,再在 Client 阶段只生成 browser bundle。tsdown 只消费 `lib/types` 中由前置 tsc 发射的 JavaScript。\n\nTypeRT 只在 Host tsdown 中以 `tsconfig.host.json` 为种子运行。它分析 Host 类型并生成 Host 反射产物及 Host-for-Client Remote 投影;Client tsdown 不启动 TypeRT。`pnpm run typecheck` 因此先执行完整 Host lib 阶段,再运行 Client tsc;`pnpm run build` 继续执行 Client tsdown 和 Web 构建。该顺序的决策记录见 [API Remotes 生成契约构建 Note](../.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.md)。\n\n静态分析和测试通过 base 的 `paths` 映射把工作区 import 解析到 `src`,且必须在干净树上通过;消费构建产物 `lib/` 的门禁显式声明该依赖。生成的 Host-for-Client Remote 声明是有意设置的例外:公共 `typecheck`、`lint` 和 `doc-typecheck` 命令会先生成这些声明,而内部 `*:contracts-ready` 脚本以调用它的公共命令或调度器门禁已经显式依赖 TypeRT 契约 pass 或完整构建为前提。双 aggregate 拓扑见 [solution-root Note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md),tsc-first 发射职责见 [ts-build-config Note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md),门禁准备契约见 [TypeRT Remote Agent Note](../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md)。\n\n业务 Service 在 Host 使用 `@Remote` 或 `@RemoteScope` 声明可调用方法;Host 构建生成 Host-for-Client 类型与运行时贡献,Client 的 `api-remotes` 组合加载这些贡献并挂到 `ctx.remote` 与作用域 `agentCtx.remote` namespace。两侧的生成产物、装配关系、SRC 开发回退和 Web 构建顺序见 [API Gateway](api-gateway.md)。\n\n如果相关的本地检查需要使用构建后的包产物,请先构建一次:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` 包含 `publint`(用构建出的 `lib/*.js` 文件校验 package 入口点)和 `verify-node-next-types`(用一个临时的 NodeNext 消费方校验构建出的声明文件)。新 worktree 在 `pnpm run build` 运行之前没有打包的 JS 和声明文件;普通提交和推送无需构建,除非所选检查会使用这些产物。\n\n### 环境变量\n\n真实的 DeepSeek 适配器和需要密钥的 agent 演示从环境变量或仓库根目录一个被 gitignore 的 `.env` 文件读取凭证:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` 可选,默认为公开 API。请勿提交真实凭证。未设置 `DEEPSEEK_API_KEY` 时,真实 API 的 e2e 套件会自动跳过。\n\n### Git 钩子\n\nlefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点:\n\n- `pre-commit` 应用仅用于格式化的 ESLint 修复,使用不加载项目的 `.oxlintrc.staged.json` 配置验证暂存文件并应用 Oxlint 的原生修复,在暂存文件属于 `THIRD_PARTY_NOTICES.md` 的输入时重新生成该文件,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫;\n- `pre-push` 运行 `pnpm run typecheck`;该命令会先完成包含 TypeRT 契约生成的完整 Host lib 阶段,再运行 Client TypeScript 检查。\n\nvendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md`。\n\n这些钩子有意不运行测试、快照、文档检查、构建或 `hygiene`。贡献者只运行一次[与改动行为相关的检查](../AGENTS.md#run-relevant-checks-locally);CI 负责全量覆盖率门禁、构建产物冒烟测试,以及 Node 22.19、24 和 26 兼容性矩阵。\n\n贡献者可以选择运行 `pnpm run check:all`,执行全面的本地门禁集。该命令独立于两个 Git 钩子,也不是对 agent 的指令。\n\n### CI 门禁\n\nkeyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。\n\n### 日常命令\n\n根目录的[贡献者说明](../AGENTS.md#commands)概述常用命令,[`package.json`](../package.json) 与 [scripts/run-gates.ts](../scripts/run-gates.ts) 则负责当前脚本和门禁清单。请选择覆盖变更表面的最小检查集。文档变更使用 `pnpm run doc-sync`;package 公开行为变更还需更新所属 README 或 JSDoc,而基于构建产物的检查需要先运行 `pnpm run build`。\n\n### 演示\n\n单次运行的 Headless coding agent 需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`:\n\n```sh\npnpm run demo:headless \"summarize this workspace\"\n```\n\n自指的 cordis 演示可以检查并修改其实时插件运行时,并需要相同的凭证(默认 `web`,也可用 `acp`):\n\n```sh\npnpm run demo:cordis\n```\n\nACP 自动化服务器通过 JSON-RPC stdio 提供全新 agent 会话,同样需要 `DEEPSEEK_API_KEY`:\n\n```sh\npnpm run demo:acp\n```\n\n### TODO 标记\n\n请使用以下三种注释标签之一标记代码中的已知问题,按紧急程度排序:\n\n- `FIXME`:应当阻塞新版本发布的问题。除非评审者明确同意该更改可以合并,否则发布版本不应包含未解决的 `FIXME`;\n- `TODO`:应当尽快修复的问题,等资源到位即可处理;\n- `XXX`:也许某天会修复的问题,优先级最低,不作承诺。\n\n请选择与紧急程度匹配的标签,让浏览代码的人一眼分清「发布阻塞」和「有空再说」。\n\n### 逐字记录类型(`ts type-equiv`)\n\n[核心数据结构](core-data-structures/core.md)文档会把与源码等价的声明及其原始 JSDoc 一并粘贴,让读者看到确切形状和源码契约。为防止粘贴内容在源码变化时漂移,请将其围栏为 ` ```ts type-equiv `(而不是 ` ```ts `),并在 `scripts/type-equiv.manifest.json` 中登记它镜像的源文件和符号:\n\n```json\n{ \"doc\": \"docs/core-data-structures/session.md\", \"symbol\": \"SessionEvent\", \"source\": \"packages/core/session/src/types.ts\" }\n```\n\n`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `\"projection\": \"public-api\"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码契约和确切形状。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。\n" + "content": "# 开发指南\n\n[English](development.md) | 中文\n\n搭建教程引导新贡献者从准备前置条件开始,直到检出通过检查。后面的贡献者参考介绍仓库布局、日常工作流和 CI 形态。设计依据与实现细节属于链接的 Agent Note 和脚本。\n\n## 搭建教程\n\n### 前置条件\n\n- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md)。\n- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。\n- Git 2.26 或更高版本;钩子设置会启用 Git 的 worktree 专属配置扩展。\n- 可选:一个 DeepSeek API key,用于 Web、headless 和 ACP(Agent Client Protocol)自动化 agent(智能体)演示以及真实 API 的 e2e 测试。\n\n### 首次搭建\n\n在仓库根目录安装依赖:\n\n```sh\npnpm install\n```\n\n安装过程还会通过 `scripts/install-lefthook.mjs` 配置 worktree 本地的 Lefthook 钩子和 `dsh-translation-pairing` Git 合并驱动。[worktree 本地钩子 Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) 负责钩子路径的安全契约;[自动配对合并 Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) 负责合并驱动。\n\n如果依赖是从缓存恢复或 `postinstall` 被跳过而导致任一集成缺失,请手动安装:\n\n```sh\nnode scripts/install-lefthook.mjs\n```\n\n如果包装脚本拒绝现有 Git 配置或报告陈旧锁,请遵循其诊断和所链接的 Agent Note,不要凭猜测编辑 worktree 元数据。移动检出目录后,请重新运行包装脚本以重新生成自有路径。\n\n新克隆后请先运行一次类型检查:\n\n```sh\npnpm run typecheck\n```\n\n`pnpm run typecheck` 成功退出即表示搭建完成。\n\n## 贡献者参考\n\n### TypeScript 项目布局\n\n仓库使用相互隔离的 Host 与 Client aggregate。普通 package 只登记进其中一个 aggregate;Host 包进入 `tsconfig.host.json`,Client 包进入 `tsconfig.client.json`。\n\n| 文件 | 角色 | 是否构成 program? |\n|---|---|---|\n| `tsconfig.json` | solution 根:`extends` base、`files: []`、引用两个 aggregate。它是 tsserver 发现入口,也是显式执行整张 Project Reference 图时的入口;经继承的 `paths` 充当 tsx 运行 `examples/` 与 `scripts/` 时的解析配置。 | 否 |\n| `tsconfig.host.json` | Host aggregate:Host package、示例、测试、脚本和 website,以及 `api/remotes` 的 Host 特例 project。 | 是 |\n| `tsconfig.client.json` | Client aggregate:`packages/client/*` package 及其测试、`apps/web`,以及 `api/remotes` 的 Client 特例 project。 | 是 |\n| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 |\n| `tsconfig.base.client.json` | 浏览器编译形状(`jsx`、DOM lib、`types: []`),由 Client aggregate 和每个 `packages/client/*` package extends。 | 否 |\n\nHost 与 Client 保持两个 aggregate program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个 aggregate,一个 paths 门面也可以横跨两侧。由此推出三条纪律:\n\n- `tsconfig.base.json` 永不添加 `include` 或 `files`:它们会泄漏进每个 extends 它的包项目,并收窄门面的全匹配范围。\n- 构造全仓 `ts.Program` 的脚本显式种子 `tsconfig.host.json` 或 `tsconfig.client.json`——永不种子根 solution,因为把两个 aggregate 展平进一个 program 会撞上 `Context` 合并冲突。\n- 新 package 只登记进一个 aggregate。包同时具有 Node loader 入口和 browser 入口并不构成拆分理由;普通 Client plugin 的两份运行时产物都在 Client 构建阶段生成。\n\n`api/remotes` 是唯一拆分 Host/Client tsconfig 的仓库特例。它的 Host 入口必须进入 Host TypeRT 图,而 Client 入口导入 Host tsdown 才会生成的 `/remote` 声明,因此本包根 `tsconfig.json` 只作为 solution,两个 aggregate 和直接消费方分别引用 `tsconfig.host.json` 或 `tsconfig.client.json`。workspace `constraints` 门禁遍历可达的 Project Reference 图,并按各引用 project 自身的 compiler face 检查:只有单一配置的目标可由任一 face 引用,拆分配置的目标则必须引用匹配的 leaf,不得引用 solution 根或另一侧 leaf。不要把该结构推广到其他包;完整边界见 [`api-remotes` README](../packages/api/remotes/README.md)。\n\n根构建按生成依赖排序:\n\n```sh\ntsc -b tsconfig.host.json\ntsdown --env.DSH_BUILD_FACE host\ntsc -b tsconfig.client.json\ntsdown --env.DSH_BUILD_FACE client\npnpm run build:web\n```\n\n两次 tsdown 都使用同一组完整 workspace 匹配,不扫描构建产物来发现 Client package,也不维护 Host/Client package 过滤表。包内 tsdown 配置根据 `DSH_BUILD_FACE` 决定当前阶段的入口:普通 Client plugin 在 Client 阶段同时生成 Node loader 与 browser bundle;`api-remotes` 通过 `hostPhase: true` 提前生成 Host 入口,再在 Client 阶段只生成 browser bundle。tsdown 只消费 `lib/types` 中由前置 tsc 发射的 JavaScript。\n\nTypeRT 只在 Host tsdown 中以 `tsconfig.host.json` 为种子运行。它分析 Host 类型并生成 Host 反射产物及 Host-for-Client Remote 投影;Client tsdown 不启动 TypeRT。`pnpm run typecheck` 因此先执行完整 Host lib 阶段,再运行 Client tsc;`pnpm run build` 继续执行 Client tsdown 和 Web 构建。该顺序的决策记录见 [API Remotes 生成契约构建 Note](../.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.md)。\n\n静态分析和测试通过 base 的 `paths` 映射把工作区 import 解析到 `src`,且必须在干净树上通过;消费构建产物 `lib/` 的门禁显式声明该依赖。生成的 Host-for-Client Remote 声明是有意设置的例外:公共 `typecheck`、`lint` 和 `doc-typecheck` 命令会先生成这些声明,而内部 `*:contracts-ready` 脚本以调用它的公共命令或调度器门禁已经显式依赖 TypeRT 契约 pass 或完整构建为前提。双 aggregate 拓扑见 [solution-root Note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md),tsc-first 发射职责见 [ts-build-config Note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md),门禁准备契约见 [TypeRT Remote Agent Note](../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md)。\n\n业务 Service 在 Host 使用 `@Remote` 或 `@RemoteScope` 声明可调用方法;Host 构建生成 Host-for-Client 类型与运行时贡献,Client 的 `api-remotes` 组合加载这些贡献并挂到 `ctx.remote` 与作用域 `agentCtx.remote` namespace。两侧的生成产物、装配关系、SRC 开发回退和 Web 构建顺序见 [API Gateway](api-gateway.md)。\n\n如果相关的本地检查需要使用构建后的包产物,请先构建一次:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` 包含 `publint`(用构建出的 `lib/*.js` 文件校验 package 入口点)和 `verify-node-next-types`(用一个临时的 NodeNext 消费方校验构建出的声明文件)。新 worktree 在 `pnpm run build` 运行之前没有打包的 JS 和声明文件;普通提交和推送无需构建,除非所选检查会使用这些产物。\n\n### 环境变量\n\n真实的 DeepSeek 适配器和需要密钥的 agent 演示从环境变量或仓库根目录一个被 gitignore 的 `.env` 文件读取凭证:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` 可选,默认为公开 API。请勿提交真实凭证。未设置 `DEEPSEEK_API_KEY` 时,真实 API 的 e2e 套件会自动跳过。\n\n### Git 集成\n\n当两种语言的文件都使用 Git 默认文本策略且能干净合并时,配对合并驱动会根据已确认的祖先、当前和另一侧的配对文档 blob,推导出发生冲突的 `.i18n.yaml` 记录。配对文档发生冲突、存在非文本合并配置或记录无效时,它会拒绝处理并保留冲突;如果合并已经因冲突而停止,请运行 `pnpm run resolve-translation-pairing-conflicts`,该命令会暂存每份可安全生成的配对记录;如果其他配对冲突仍需手工处理,则以非零状态退出。确切边界见[双语文档契约](i18n/README.md#the-pairing-contract)。\n\n安装脚本在发布 worktree 配置前,会探测确切的 Node/tsx 驱动入口点。如果该运行时之后变得不可用,不依赖 Node 的启动器会写入 Git 的普通文本合并结果、让伴随文件保持未解决状态,并打印恢复路径;请恢复依赖后运行 `pnpm run resolve-translation-pairing-conflicts`,或运行 `git merge --abort`。如果 `pre-merge-commit` 拒绝原本能干净完成的合并,Git 会把完整结果留在暂存区但不创建提交;请修复失败后运行 `git commit`,或中止合并。确切的索引与 `MERGE_HEAD` 状态由[自动配对合并 Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md#failure-contract)负责记录。\n\nlefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点:\n\n- `pre-commit` 对照暂存的配对文档 blob 校验暂存的配对记录,应用仅用于格式化的 ESLint 修复,使用不加载项目的 `.oxlintrc.staged.json` 配置验证暂存文件并应用 Oxlint 的原生修复,在暂存文件属于 `THIRD_PARTY_NOTICES.md` 的输入时重新生成该文件,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫;\n- `pre-merge-commit` 在 Git 创建自动合并提交前执行同样以索引为准的配对检查;\n- `pre-push` 运行 `pnpm run typecheck`;该命令会先完成包含 TypeRT 契约生成的完整 Host lib 阶段,再运行 Client TypeScript 检查。\n\nvendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md`。\n\n除限定范围的暂存记录校验外,这些钩子有意不运行测试、快照、文档检查、构建或 `hygiene`。贡献者只运行一次[与改动行为相关的检查](../AGENTS.md#run-relevant-checks-locally);CI 负责全量覆盖率门禁、构建产物冒烟测试,以及 Node 22.19、24 和 26 兼容性矩阵。\n\n贡献者可以选择运行 `pnpm run check:all`,执行全面的本地门禁集。该命令独立于 Git 钩子,也不是对 agent 的指令。\n\n### CI 门禁\n\nkeyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。\n\n### 日常命令\n\n根目录的[贡献者说明](../AGENTS.md#commands)概述常用命令,[`package.json`](../package.json) 与 [scripts/run-gates.ts](../scripts/run-gates.ts) 则负责当前脚本和门禁清单。请选择覆盖变更表面的最小检查集。文档变更使用 `pnpm run doc-sync`;package 公开行为变更还需更新所属 README 或 JSDoc,而基于构建产物的检查需要先运行 `pnpm run build`。\n\n### 演示\n\n单次运行的 Headless coding agent 需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`:\n\n```sh\npnpm run demo:headless \"summarize this workspace\"\n```\n\n自指的 cordis 演示可以检查并修改其实时插件运行时,并需要相同的凭证(默认 `web`,也可用 `acp`):\n\n```sh\npnpm run demo:cordis\n```\n\nACP 自动化服务器通过 JSON-RPC stdio 提供全新 agent 会话,同样需要 `DEEPSEEK_API_KEY`:\n\n```sh\npnpm run demo:acp\n```\n\n### TODO 标记\n\n请使用以下三种注释标签之一标记代码中的已知问题,按紧急程度排序:\n\n- `FIXME`:应当阻塞新版本发布的问题。除非评审者明确同意该更改可以合并,否则发布版本不应包含未解决的 `FIXME`;\n- `TODO`:应当尽快修复的问题,等资源到位即可处理;\n- `XXX`:也许某天会修复的问题,优先级最低,不作承诺。\n\n请选择与紧急程度匹配的标签,让浏览代码的人一眼分清「发布阻塞」和「有空再说」。\n\n### 逐字记录类型(`ts type-equiv`)\n\n[子系统](subsystems/README.md)页面会把与源码等价的声明及其原始 JSDoc 一并粘贴,让读者看到确切形状和源码契约。为防止粘贴内容在源码变化时漂移,请将其围栏为 ` ```ts type-equiv `(而不是 ` ```ts `),并在 `scripts/type-equiv.manifest.json` 中登记它镜像的源文件和符号:\n\n```json\n{ \"doc\": \"docs/subsystems/session.md\", \"symbol\": \"SessionEvent\", \"source\": \"packages/core/session/src/types.ts\" }\n```\n\n`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `\"projection\": \"public-api\"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码契约和确切形状。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。\n" }, { "role": "user", - "content": "# Bilingual documentation\n\nEnglish | [中文](README.zh.md)\n\nThis repo's documentation is read by people and agents both inside and outside the company, so every document in scope is maintained in English and Simplified Chinese. This page defines the pairing contract, enforcement gate, scope, and exclusions; [translation-rules.md](translation-rules.md) defines how to translate; [terminology.md](terminology.md) is the terminology source of truth. The committed agent workflow lives in [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md).\n\n## The pairing contract\n\n- **Both languages carry equal authority.** A document may be authored and reviewed in either language first — a Chinese-first Agent Note is as legitimate as an English-first one — and the counterpart is translated from it. Neither file outranks the other; what binds them is that they must say the same thing.\n- **A pair is three sibling files.** The English `foo.md`, the Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`, all in the same directory. No locale directories, no separate translation repo, no interleaved bilingual files. Pairs merge whole: a PR never lands one language without the other two files.\n- **The consistency record.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last time the two were confirmed to say the same thing:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. `--write` stores those snapshots in the local Git object database before recording them, including uncommitted working-tree contents, and pins every distinct stored blob under a content-addressed `refs/dsh/translation-pairing/snapshots/` ref so garbage collection cannot invalidate a recorded recovery pointer. The recorded hashes therefore recover the exact last-confirmed text of either side, so an out-of-sync pair is updated by patching the counterpart minimally against the edited side's diff — never by re-translating whole files. `pnpm run gen-translation-brief ` assembles that update's working set mechanically at the narrowest safely aligned granularity — changed Markdown units, then heading sections, then whole document — with the edited side's diff since last confirmation, each changed span's three-way text, the terminology rows the change touches, and the binding update rules; a change confined to the pair's byte-identical code fences is computed outright, and `--apply` splices it into the counterpart after structural validation ([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md)). After bringing the pair back in line, `pnpm run verify-translation-pairing --write ` re-records both hashes; that yaml diff is the reviewable act of confirming consistency, which is why `--write` requires naming the pairs you confirmed (`--write --all` is the explicit corpus-wide form).\n- **Language switcher.** Both files link to each other immediately after their H1 heading: the English file carries `English | [中文](foo.zh.md)` and the Chinese file carries `[English](foo.md) | 中文`.\n- **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, link targets, and verbatim code blocks match one to one across the pair — see [translation-rules.md](translation-rules.md) for the full preservation rules. Existing Markdown gates apply to `.zh.md` files unchanged (`verify-md-wrap`, `verify-md-links`).\n\n## The gate: verify-translation-pairing\n\n`pnpm run verify-translation-pairing` (part of `doc-sync`, which contributors run locally for documentation changes and CI runs exhaustively) enforces the contract mechanically:\n\n1. Every document in scope has a complete pair. README discovery is case-insensitive on the basename, so `missions/readme.md` is in scope alongside the other documentation roots.\n2. Every pair artifact that exists at all is complete and consistent: all three files present, each side's current blob hash equals the recorded one (editing either side without re-confirming the pair goes red), both sides carry the language switcher, and the structural signatures match in order — heading depths, verbatim code blocks (info string and content), table row and column counts, list kinds, ordered-list starts, item counts, and every link target apart from the switcher.\n3. Files listed as `excluded` have no `.zh.md` and no `.i18n.yaml` at all. Frozen Agent Notes under `.agents/notes/archived/` are outside this evolving gate; their dedicated verifier requires and seals the complete existing triplet instead.\n\nSource-oriented code gates consume an exact `.zh.md` fence sequence as a derivative of its unsuffixed sibling instead of compiling or manifesting the same code twice. The sequence must match in length, order, fence kind, and byte-exact body; otherwise both copies remain independently checked and the pairing gate reports the structural mismatch.\n\n`pnpm run verify-translation-pairing --list` prints the current pairing state of every document in scope — missing, out-of-sync, or ok. It never fails; `missing` and `out-of-sync` rows identify violations that the normal check rejects.\n\n`pnpm run verify-translation-pairing ` checks just the named pairs — any of a pair's three files (or its bare stem) names it — so an update loop verifies its own pair in seconds instead of re-scanning the corpus. The no-argument corpus-wide form is what `doc-sync` and CI run; a scoped green never substitutes for it at PR level.\n\nThe practical rule this gate creates: **when a PR edits either side of a paired document, the same PR updates the counterpart and re-records the pair** (run the [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill, then `--write `), exactly like the repo's existing doc-sync rule for code and READMEs. A PR that leaves a pair out of sync goes red in CI.\n\nThe gate's limit, stated plainly: **a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.** It checks hashes and shape; it cannot judge whether the two sides actually say the same thing, or whether the wording is accurate, well-termed, and natural — that is the reviewer's half of the contract, per [translation-rules.md](translation-rules.md). A re-recorded pair with a sloppy counterpart passes the gate; it must not pass review.\n\n## Scope and exclusions\n\n**Scope**: every non-vendor README, plus every active document under `.agents/notes/**`, `docs/**`, and `python/**`. README matching is case-insensitive on the basename and covers future directories without another manifest edit. Dependency and ignored build-output trees and the frozen `.agents/notes/archived/` tree are discovery exclusions, not evolving translation source.\n\n**Excluded** (never paired, and the gate rejects a `.zh.md` or `.i18n.yaml` for them):\n\n- `docs/cordis-catalog/`, `docs/tool-catalog/`, `docs/config-catalog.md`, `docs/persistence-catalog.md`, `docs/module-graph.md`, `docs/agent-lifecycle.md`, `docs/capability-seams.md`, `docs/event-producer-consumer.md`, `docs/graph-atlas.md`, and `docs/tool-execution-pipeline.md` — generated files whose generators emit English only; a hand-written translation would go stale on regeneration.\n- `docs/AGENTS.md`, `.agents/notes/**/AGENTS.md`, and their `CLAUDE.md` instruction symlinks — agent instructions, maintained in English only like the root `AGENTS.md`.\n- `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction.\n- [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior.\n- `.agents/notes/archived/` — frozen historical triplets. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) validates their completeness and content seals; translation maintenance must never rewrite them.\n\n**Universal requirement**: every current or future document in scope must merge as a complete bilingual pair. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) contains only explicit exclusions; there is no per-file rollout list, date cutoff, or README-specific policy class.\n\n## Division of labor\n\nCounterparts here are produced by an agent running [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) and reviewed by a human — inference is cheap here, review attention is the scarce resource. The gate checks pair completeness, recorded hashes, switchers, and its documented structural signature. Review still owns translation quality, terminology, and structural requirements that the signature does not encode. The prompt contract is executable: [scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) renders the committed template (terminology injected; the template carries its own calibrated rules) into either direction and parses the three-section response, while `verify-translation-prompt` exercises both render directions and the checked-in example in `doc-sync`.\n" + "content": "# Bilingual documentation\n\nEnglish | [中文](README.zh.md)\n\nThis repo's documentation is read by people and agents both inside and outside the company, so every document in scope is maintained in English and Simplified Chinese. This page defines the pairing contract, enforcement gate, scope, and exclusions; [translation-rules.md](translation-rules.md) defines how to translate; [terminology.md](terminology.md) is the terminology source of truth. The committed agent workflow lives in [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md).\n\n## The pairing contract\n\n- **Both languages carry equal authority.** A document may be authored and reviewed in either language first — a Chinese-first Agent Note is as legitimate as an English-first one — and the counterpart is translated from it. Neither file outranks the other; what binds them is that they must say the same thing.\n- **A pair is three sibling files.** The English `foo.md`, the Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`, all in the same directory. No locale directories, no separate translation repo, no interleaved bilingual files. Pairs merge whole: a PR never lands one language without the other two files.\n- **The consistency record.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last time the two were confirmed to say the same thing:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. `--write` stores those snapshots in the local Git object database before recording them, including uncommitted working-tree contents, and pins every distinct stored blob under a content-addressed `refs/dsh/translation-pairing/snapshots/` ref so garbage collection cannot invalidate a recorded recovery pointer. The recorded hashes therefore recover the exact last-confirmed text of either side, so an out-of-sync pair is updated by patching the counterpart minimally against the edited side's diff — never by re-translating whole files. `pnpm run gen-translation-brief ` assembles that update's working set mechanically at the narrowest safely aligned granularity — changed Markdown units, then heading sections, then whole document — with the edited side's diff since last confirmation, each changed span's three-way text, the terminology rows the change touches, and the binding update rules; a change confined to the pair's byte-identical code fences is computed outright, and `--apply` splices it into the counterpart after structural validation ([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md)). After bringing the pair back in line, `pnpm run verify-translation-pairing --write ` re-records both hashes; that yaml diff is the reviewable act of confirming consistency, which is why `--write` requires naming the pairs you confirmed (`--write --all` is the explicit corpus-wide form).\n\n When two branches contain valid confirmations of the same pair, the installed `dsh-translation-pairing` Git merge driver composes a new record only if Git's default text merge succeeds for both recorded owner-blob triplets and the merged pair retains its switchers and structural signature. Any uncertain shape remains an ordinary conflict; `pnpm run resolve-translation-pairing-conflicts` applies the same fail-closed operation to a merge that has already stopped, stages every safe pairing record, and exits unsuccessfully when other pairing conflicts remain. The [automatic pairing merges Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) owns the mechanism and alternatives.\n- **Language switcher.** Both files link to each other immediately after their H1 heading: the English file carries `English | [中文](foo.zh.md)` and the Chinese file carries `[English](foo.md) | 中文`.\n- **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, link targets, and verbatim code blocks match one to one across the pair — see [translation-rules.md](translation-rules.md) for the full preservation rules. Existing Markdown gates apply to `.zh.md` files unchanged (`verify-md-wrap`, `verify-md-links`).\n\n## The gate: verify-translation-pairing\n\n`pnpm run verify-translation-pairing` (part of `doc-sync`, which contributors run locally for documentation changes and CI runs exhaustively) enforces the contract mechanically:\n\n1. Every document in scope has a complete pair. README discovery is case-insensitive on the basename, so `missions/readme.md` is in scope alongside the other documentation roots.\n2. Every pair artifact that exists at all is complete and consistent: all three files present, each side's current blob hash equals the recorded one (editing either side without re-confirming the pair goes red), both sides carry the language switcher, and the structural signatures match in order — heading depths, verbatim code blocks (info string and content), table row and column counts, list kinds, ordered-list starts, item counts, and every link target apart from the switcher.\n3. Files listed as `excluded` have no `.zh.md` and no `.i18n.yaml` at all. Frozen Agent Notes under `.agents/notes/archived/` are outside this evolving gate; their dedicated verifier requires and seals the complete existing triplet instead.\n\nSource-oriented code gates consume an exact `.zh.md` fence sequence as a derivative of its unsuffixed sibling instead of compiling or manifesting the same code twice. The sequence must match in length, order, fence kind, and byte-exact body; otherwise both copies remain independently checked and the pairing gate reports the structural mismatch.\n\n`pnpm run verify-translation-pairing --list` prints the current pairing state of every document in scope — missing, out-of-sync, or ok. It never fails; `missing` and `out-of-sync` rows identify violations that the normal check rejects.\n\n`pnpm run verify-translation-pairing ` checks just the named pairs — any of a pair's three files (or its bare stem) names it — so an update loop verifies its own pair in seconds instead of re-scanning the corpus. The no-argument corpus-wide form is what `doc-sync` and CI run; a scoped green never substitutes for it at PR level.\n\nThe practical rule this gate creates: **when a PR edits either side of a paired document, the same PR updates the counterpart and re-records the pair** (run the [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill, then `--write `), exactly like the repo's existing doc-sync rule for code and READMEs. A PR that leaves a pair out of sync goes red in CI.\n\nThe gate's limit, stated plainly: **a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.** It checks hashes and shape; it cannot judge whether the two sides actually say the same thing, or whether the wording is accurate, well-termed, and natural — that is the reviewer's half of the contract, per [translation-rules.md](translation-rules.md). A re-recorded pair with a sloppy counterpart passes the gate; it must not pass review.\n\n## Scope and exclusions\n\n**Scope**: every non-vendor README, plus every active document under `.agents/notes/**`, `docs/**`, and `python/**`. README matching is case-insensitive on the basename and covers future directories without another manifest edit. Dependency and ignored build-output trees and the frozen `.agents/notes/archived/` tree are discovery exclusions, not evolving translation source.\n\n**Excluded** (never paired, and the gate rejects a `.zh.md` or `.i18n.yaml` for them):\n\n- `docs/cordis-catalog/`, `docs/tool-catalog/`, `docs/config-catalog.md`, `docs/persistence-catalog.md`, `docs/module-graph.md`, `docs/agent-lifecycle.md`, `docs/capability-seams.md`, `docs/event-producer-consumer.md`, `docs/graph-atlas.md`, and `docs/tool-execution-pipeline.md` — generated files whose generators emit English only; a hand-written translation would go stale on regeneration.\n- `docs/AGENTS.md`, `.agents/notes/**/AGENTS.md`, and their `CLAUDE.md` instruction symlinks — agent instructions, maintained in English only like the root `AGENTS.md`.\n- `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction.\n- [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior.\n- `.agents/notes/archived/` — frozen historical triplets. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) validates their completeness and content seals; translation maintenance must never rewrite them.\n\n**Universal requirement**: every current or future document in scope must merge as a complete bilingual pair. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) contains only explicit exclusions; there is no per-file rollout list, date cutoff, or README-specific policy class.\n\n## Division of labor\n\nCounterparts here are produced by an agent running [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) and reviewed by a human — inference is cheap here, review attention is the scarce resource. The gate checks pair completeness, recorded hashes, switchers, and its documented structural signature. Review still owns translation quality, terminology, and structural requirements that the signature does not encode. The prompt contract is executable: [scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) renders the committed template (terminology injected; the template carries its own calibrated rules) into either direction and parses the three-section response, while `verify-translation-prompt` exercises both render directions and the checked-in example in `doc-sync`.\n" }, { "role": "assistant", - "content": "# 双语文档\n\n[English](README.md) | 中文\n\n本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此范围内的每篇文档都以英文和简体中文维护。本页定义配对契约、强制门禁、范围与排除规则;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。仓库内置的 agent 工作流见 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。\n\n## 配对契约\n\n- **两种语言同权。** 一篇文档可以先用任一语言撰写和评审(先写中文的 Agent Note 与先写英文的一样正当),另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。\n- **一对文档是三个同目录文件。** 英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对必须整体合并:PR(Pull Request)永远不会只带一种语言而缺其余两个文件。\n- **一致性记录。**`foo.i18n.yaml` 保存两侧文件在上一次被确认「说同样的话」时各自的完整 git blob hash:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。`--write` 会先把这些快照存入本地 Git 对象库再写下记录,未提交的工作树内容也不例外;它还会在内容寻址的 `refs/dsh/translation-pairing/snapshots/` ref 下固定每个不同的已存 blob,使垃圾回收无法让已记录的恢复指针失效。因此记录的 hash 能还原任一侧上次确认时的确切文本,所以失去同步的配对是「按被改一侧的 diff 最小化地修补另一侧」,从不整篇重译。`pnpm run gen-translation-brief ` 会以能安全对齐的最窄粒度——先是有改动的 Markdown 单元,再是标题小节,最后是整篇文档——机械地汇集这次更新的工作集:被改一侧自上次确认以来的 diff、每个改动块的三方文本、改动触及的术语表行,以及有约束力的更新规则;仅落在配对中逐字节一致的围栏代码块内的改动可以直接算出,`--apply` 则经结构签名校验后把它拼接进对侧文件([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md))。两侧对齐后,`pnpm run verify-translation-pairing --write ` 重新记录两个 hash;那份 yaml diff 就是「确认一致」这个动作本身,可以被评审,也正因如此,`--write` 要求点名你确认过的配对(`--write --all` 是显式的全语料形式)。\n- **语言切换行。** 两个文件在各自 H1 标题之后立即互链:英文文件带 `English | [中文](foo.zh.md)`,中文文件带 `[English](foo.md) | 中文`。\n- **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、链接目标与逐字节一致的代码块在配对两侧一一对应;完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。\n\n## 门禁:verify-translation-pairing\n\n`pnpm run verify-translation-pairing`(`doc-sync`(文档同步门禁)的一环,贡献者会针对文档变更在本地运行,CI 则会完整运行)机械地强制执行这份契约:\n\n1. 范围内的每篇文档都有完整配对。发现 README 时,basename 不区分大小写,因此 `missions/readme.md` 与其他文档根一样属于范围。\n2. 任何已存在的配对产物都完整且一致:三个文件齐全、每一侧的当前 blob hash 等于记录值(改了任一侧而没重新确认配对就变红)、双方都带语言切换行、结构签名按序一致:标题深度、逐字节一致的代码块(信息字符串与内容)、表格行列数、列表类型、有序列表起始编号、列表项数量,以及除切换行之外的每个链接目标。\n3. 列为 `excluded` 的文件完全没有 `.zh.md`,也没有 `.i18n.yaml`。`.agents/notes/archived/` 下冻结的 Agent Note 不受这个持续演进的门禁约束;专用校验器会要求其现有的三个配对文件完整,并将其封存。\n\n面向源码的代码门禁会把精确的 `.zh.md` 围栏序列视为其无后缀兄弟文件的派生内容,而不会再次编译相同代码或在 manifest 中重复登记。该序列必须在长度、顺序、围栏类型和按字节精确的正文上一致;否则两份副本仍会独立受检,配对门禁也会报告结构不匹配。\n\n`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok)。它从不失败;其中 missing 与 out-of-sync 行指出普通检查会拒绝的违规。\n\n`pnpm run verify-translation-pairing ` 只检查被点名的配对——配对的三个文件中的任意一个(或其裸词干)都能点名它——因此更新循环几秒内就能验证自己的配对,而不必重新扫描全语料。`doc-sync` 与 CI 运行的是无参数的全语料形式;限定范围的绿灯在 PR 层面永远不能替代它。\n\n这个门禁带来的实际规则是:**当一个 PR 修改了已配对文档的任一侧时,同一个 PR 更新另一侧并重新记录配对**(运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill(技能),再 `--write `),与本仓库既有的代码与 README 的 doc-sync 规则完全一致。留下失去同步的配对的 PR 会在 CI 变红。\n\n把门禁的边界说白:**门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。** 它检查记录的 hash 与结构签名;它无法判断两侧是否真的在说同样的话,也无法判断措辞是否准确、术语是否得当、行文是否自然;这部分契约由评审者把关,见 [translation-rules.md](translation-rules.md)。重新记录了 hash 但另一侧翻得潦草的配对能通过门禁;它不得通过评审。\n\n## 范围与排除\n\n**范围**:除 vendor 源码外的全部 README,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部活跃文档。匹配 README 时只看文件名且不区分大小写,因此今后新增的目录无需再修改 manifest。依赖目录、被忽略的构建产物目录以及冻结的 `.agents/notes/archived/` 目录树只在发现阶段排除,不属于持续演进的翻译源文档。\n\n**排除**(永不配对,门禁拒绝为它们建 `.zh.md` 或 `.i18n.yaml`):\n\n- `docs/cordis-catalog/`、`docs/tool-catalog/`、`docs/config-catalog.md`、`docs/persistence-catalog.md`、`docs/module-graph.md`、`docs/agent-lifecycle.md`、`docs/capability-seams.md`、`docs/event-producer-consumer.md`、`docs/graph-atlas.md` 与 `docs/tool-execution-pipeline.md`:生成文件,其生成器只输出英文;手写译文会在重新生成时变得陈旧。\n- `docs/AGENTS.md`、`.agents/notes/**/AGENTS.md` 以及指向它们的 `CLAUDE.md` 指令符号链接:agent 指令,与根 `AGENTS.md` 一样只以英文维护。\n- `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md):二者本身即为中英对照文档。\n- [translation-prompt.md](translation-prompt.md):自动翻译流水线的提示词模板;正文逐字进入模型请求,配对翻译会改变流水线行为。\n- `.agents/notes/archived/`:冻结的历史三文件配对。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 校验其完整性和内容封存记录;翻译维护绝不能重写这些文件。\n\n**统一要求**:当前及今后纳入范围的每篇文档,合并时都必须构成完整的双语配对。[scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 只包含显式排除项;不存在逐文件推进清单、日期分界或 README 专用政策类别。\n\n## 分工\n\n这里的对侧文件由运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 的 agent 生成,再由人评审:在这里推理(inference)很便宜,评审注意力才是稀缺资源。门禁负责检查配对是否完整、记录的 hash、语言切换行以及本文列出的结构签名;翻译质量、术语和签名未涵盖的结构要求仍由评审把关。提示词契约也有可执行实现:[scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) 会把仓库内置的模板(注入术语表;模板自带经人工校准的规则)渲染为英译中或中译英两个方向的提示词,并解析三段式响应;`doc-sync` 中的 `verify-translation-prompt` 会检查两个渲染方向与仓库内示例。\n" + "content": "# 双语文档\n\n[English](README.md) | 中文\n\n本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此范围内的每篇文档都以英文和简体中文维护。本页定义配对契约、强制门禁、范围与排除规则;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。仓库内置的 agent 工作流见 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。\n\n## 配对契约\n\n- **两种语言同权。** 一篇文档可以先用任一语言撰写和评审(先写中文的 Agent Note 与先写英文的一样正当),另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。\n- **一对文档是三个同目录文件。** 英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对必须整体合并:PR(Pull Request)永远不会只带一种语言而缺其余两个文件。\n- **一致性记录。**`foo.i18n.yaml` 保存两侧文件在上一次被确认「说同样的话」时各自的完整 git blob hash:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。`--write` 会先把这些快照存入本地 Git 对象库再写下记录,未提交的工作树内容也不例外;它还会在内容寻址的 `refs/dsh/translation-pairing/snapshots/` ref 下固定每个不同的已存 blob,使垃圾回收无法让已记录的恢复指针失效。因此记录的 hash 能还原任一侧上次确认时的确切文本,所以失去同步的配对是「按被改一侧的 diff 最小化地修补另一侧」,从不整篇重译。`pnpm run gen-translation-brief ` 会以能安全对齐的最窄粒度——先是有改动的 Markdown 单元,再是标题小节,最后是整篇文档——机械地汇集这次更新的工作集:被改一侧自上次确认以来的 diff、每个改动块的三方文本、改动触及的术语表行,以及有约束力的更新规则;仅落在配对中逐字节一致的围栏代码块内的改动可以直接算出,`--apply` 则经结构签名校验后把它拼接进对侧文件([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md))。两侧对齐后,`pnpm run verify-translation-pairing --write ` 重新记录两个 hash;那份 yaml diff 就是「确认一致」这个动作本身,可以被评审,也正因如此,`--write` 要求点名你确认过的配对(`--write --all` 是显式的全语料形式)。\n\n 当两个分支都包含同一配对的有效确认时,已安装的 `dsh-translation-pairing` Git 合并驱动只会在 Git 默认文本合并能分别干净合并记录所指向的英文三方 blob 与中文三方 blob,且合并后的配对仍保留两侧的语言切换行和结构签名时,组合出一份新记录。任何无法确定的情形都保留为普通冲突;`pnpm run resolve-translation-pairing-conflicts` 会对已经停止的合并执行同一套遇错即保留冲突的操作,暂存每份可安全生成的配对记录,并在还有其他配对冲突时以非零状态退出。[自动配对合并 Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) 负责记录该机制与备选方案。\n- **语言切换行。** 两个文件在各自 H1 标题之后立即互链:英文文件带 `English | [中文](foo.zh.md)`,中文文件带 `[English](foo.md) | 中文`。\n- **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、链接目标与逐字节一致的代码块在配对两侧一一对应;完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。\n\n## 门禁:verify-translation-pairing\n\n`pnpm run verify-translation-pairing`(`doc-sync`(文档同步门禁)的一环,贡献者会针对文档变更在本地运行,CI 则会完整运行)机械地强制执行这份契约:\n\n1. 范围内的每篇文档都有完整配对。发现 README 时,basename 不区分大小写,因此 `missions/readme.md` 与其他文档根一样属于范围。\n2. 任何已存在的配对产物都完整且一致:三个文件齐全、每一侧的当前 blob hash 等于记录值(改了任一侧而没重新确认配对就变红)、双方都带语言切换行、结构签名按序一致:标题深度、逐字节一致的代码块(信息字符串与内容)、表格行列数、列表类型、有序列表起始编号、列表项数量,以及除切换行之外的每个链接目标。\n3. 列为 `excluded` 的文件完全没有 `.zh.md`,也没有 `.i18n.yaml`。`.agents/notes/archived/` 下冻结的 Agent Note 不受这个持续演进的门禁约束;专用校验器会要求其现有的三个配对文件完整,并将其封存。\n\n面向源码的代码门禁会把精确的 `.zh.md` 围栏序列视为其无后缀兄弟文件的派生内容,而不会再次编译相同代码或在 manifest 中重复登记。该序列必须在长度、顺序、围栏类型和按字节精确的正文上一致;否则两份副本仍会独立受检,配对门禁也会报告结构不匹配。\n\n`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok)。它从不失败;其中 missing 与 out-of-sync 行指出普通检查会拒绝的违规。\n\n`pnpm run verify-translation-pairing ` 只检查被点名的配对——配对的三个文件中的任意一个(或其裸词干)都能点名它——因此更新循环几秒内就能验证自己的配对,而不必重新扫描全语料。`doc-sync` 与 CI 运行的是无参数的全语料形式;限定范围的绿灯在 PR 层面永远不能替代它。\n\n这个门禁带来的实际规则是:**当一个 PR 修改了已配对文档的任一侧时,同一个 PR 更新另一侧并重新记录配对**(运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill(技能),再 `--write `),与本仓库既有的代码与 README 的 doc-sync 规则完全一致。留下失去同步的配对的 PR 会在 CI 变红。\n\n把门禁的边界说白:**门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。** 它检查记录的 hash 与结构签名;它无法判断两侧是否真的在说同样的话,也无法判断措辞是否准确、术语是否得当、行文是否自然;这部分契约由评审者把关,见 [translation-rules.md](translation-rules.md)。重新记录了 hash 但另一侧翻得潦草的配对能通过门禁;它不得通过评审。\n\n## 范围与排除\n\n**范围**:除 vendor 源码外的全部 README,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部活跃文档。匹配 README 时只看文件名且不区分大小写,因此今后新增的目录无需再修改 manifest。依赖目录、被忽略的构建产物目录以及冻结的 `.agents/notes/archived/` 目录树只在发现阶段排除,不属于持续演进的翻译源文档。\n\n**排除**(永不配对,门禁拒绝为它们建 `.zh.md` 或 `.i18n.yaml`):\n\n- `docs/cordis-catalog/`、`docs/tool-catalog/`、`docs/config-catalog.md`、`docs/persistence-catalog.md`、`docs/module-graph.md`、`docs/agent-lifecycle.md`、`docs/capability-seams.md`、`docs/event-producer-consumer.md`、`docs/graph-atlas.md` 与 `docs/tool-execution-pipeline.md`:生成文件,其生成器只输出英文;手写译文会在重新生成时变得陈旧。\n- `docs/AGENTS.md`、`.agents/notes/**/AGENTS.md` 以及指向它们的 `CLAUDE.md` 指令符号链接:agent 指令,与根 `AGENTS.md` 一样只以英文维护。\n- `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md):二者本身即为中英对照文档。\n- [translation-prompt.md](translation-prompt.md):自动翻译流水线的提示词模板;正文逐字进入模型请求,配对翻译会改变流水线行为。\n- `.agents/notes/archived/`:冻结的历史三文件配对。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 校验其完整性和内容封存记录;翻译维护绝不能重写这些文件。\n\n**统一要求**:当前及今后纳入范围的每篇文档,合并时都必须构成完整的双语配对。[scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 只包含显式排除项;不存在逐文件推进清单、日期分界或 README 专用政策类别。\n\n## 分工\n\n这里的对侧文件由运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 的 agent 生成,再由人评审:在这里推理(inference)很便宜,评审注意力才是稀缺资源。门禁负责检查配对是否完整、记录的 hash、语言切换行以及本文列出的结构签名;翻译质量、术语和签名未涵盖的结构要求仍由评审把关。提示词契约也有可执行实现:[scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) 会把仓库内置的模板(注入术语表;模板自带经人工校准的规则)渲染为英译中或中译英两个方向的提示词,并解析三段式响应;`doc-sync` 中的 `verify-translation-prompt` 会检查两个渲染方向与仓库内示例。\n" }, { "role": "user", diff --git a/scripts/translation-pairing-git.ts b/scripts/translation-pairing-git.ts index 992f0abbb9..cda27426c6 100644 --- a/scripts/translation-pairing-git.ts +++ b/scripts/translation-pairing-git.ts @@ -5,6 +5,9 @@ import { createHash } from 'node:crypto' const SNAPSHOT_REF_PREFIX = 'refs/dsh/translation-pairing/snapshots' +/** Maximum buffered stdout or stderr for repository-owned Git subprocesses. */ +export const GIT_COMMAND_MAX_BUFFER = 1 << 26 + /** Full SHA-1 Git blob hash (the 40-hex format used by pairing records). */ export function gitBlobHash(content: Buffer): string { const hash = createHash('sha1') @@ -13,10 +16,20 @@ export function gitBlobHash(content: Buffer): string { return hash.digest('hex') } -function runGit(root: string, args: string[], operation: string, input?: Buffer): Buffer { +/** + * Run one Git subprocess and return its exact stdout bytes. + * + * @param root - Repository root used as Git's working directory. + * @param args - Arguments following the `git` executable. + * @param operation - Human-readable operation for failure diagnostics. + * @param input - Optional stdin bytes. + * @returns Exact stdout bytes. + * @throws Error when Git cannot start or exits unsuccessfully. + */ +export function runGit(root: string, args: string[], operation: string, input?: Buffer): Buffer { const result = spawnSync('git', ['-C', root, ...args], { input, - maxBuffer: 1 << 26, + maxBuffer: GIT_COMMAND_MAX_BUFFER, }) if (result.error) { throw new Error(`${operation} failed: ${result.error.message}`, { cause: result.error }) @@ -27,6 +40,39 @@ function runGit(root: string, args: string[], operation: string, input?: Buffer) return result.stdout } +/** One regular stage-zero Git index entry and its exact blob bytes. */ +export interface GitIndexBlob { + /** Object ID recorded in the index. */ + objectId: string + /** Blob bytes stored under that object ID. */ + content: Buffer +} + +/** + * Read one path from the Git index without consulting working-tree bytes. + * + * @param root - Repository root. + * @param path - Repository-relative path. + * @returns The stage-zero blob, or `undefined` when the path is absent. + * @throws Error when the path is unmerged or has an invalid index shape. + */ +export function readGitIndexBlob(root: string, path: string): GitIndexBlob | undefined { + const output = runGit( + root, + ['ls-files', '--stage', '-z', '--', path], + `git ls-files --stage for ${path}`, + ).toString('utf8') + const entries = output.split('\0').filter(Boolean) + if (entries.length === 0) return undefined + if (entries.length !== 1) throw new Error(`${path} does not have exactly one resolved index entry`) + const match = /^(?:\d+) ([0-9a-f]+) 0\t[\s\S]+$/.exec(entries[0] ?? '') + if (!match?.[1]) throw new Error(`${path} remains unmerged or has an invalid index entry`) + return { + objectId: match[1], + content: runGit(root, ['cat-file', 'blob', match[1]], `reading staged ${path}`), + } +} + /** * Persist exact working-tree bytes so a pairing record can later recover them * with `git cat-file`, even when they have never appeared in the index or a diff --git a/scripts/translation-pairing-merge.spec.ts b/scripts/translation-pairing-merge.spec.ts new file mode 100644 index 0000000000..118a2023b9 --- /dev/null +++ b/scripts/translation-pairing-merge.spec.ts @@ -0,0 +1,508 @@ +/** Integration coverage for automatic and explicit pairing-record conflict resolution. */ + +import { execFileSync, spawnSync } from 'node:child_process' +import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { delimiter, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { afterEach, describe, expect, it } from 'vitest' +import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts' +import { + mergeTranslationPairingRecords, + resolveTranslationPairingConflicts, +} from './translation-pairing-merge.ts' +import { + renderTranslationPairingRecord, + translationPairPaths, +} from './translation-pairing-record.ts' + +const driver = fileURLToPath(new URL('./merge-translation-pairing.ts', import.meta.url)) +const driverLauncher = fileURLToPath(new URL('./merge-translation-pairing-driver.sh', import.meta.url)) +const workspaceRoot = fileURLToPath(new URL('../', import.meta.url)) +const tsxLoader = fileURLToPath(import.meta.resolve('tsx/esm')) +const fixtures: string[] = [] + +interface Fixture { + env: NodeJS.ProcessEnv + root: string +} + +afterEach(() => { + for (const fixture of fixtures.splice(0)) rmSync(fixture, { recursive: true, force: true }) +}) + +function git(fixture: Fixture, args: string[]): string { + return execFileSync('git', ['-C', fixture.root, ...args], { + encoding: 'utf8', + env: fixture.env, + }).trim() +} + +function write(root: string, path: string, content: string): void { + const absolute = join(root, path) + mkdirSync(dirname(absolute), { recursive: true }) + writeFileSync(absolute, content) +} + +function shellQuote(value: string): string { + return `"${value.replace(/["\\$`]/g, '\\$&')}"` +} + +function installFixtureRuntime(root: string): void { + const linkType = process.platform === 'win32' ? 'junction' : 'dir' + symlinkSync( + join(workspaceRoot, 'node_modules'), + join(root, 'node_modules'), + linkType, + ) + symlinkSync(join(workspaceRoot, 'scripts'), join(root, 'scripts'), linkType) +} + +function startMergeWithFakeNode( + fixture: Fixture, + nodeScript = '#!/bin/sh\nexit 72\n', +) { + const fakeBin = join(fixture.root, 'fake-bin') + const fakeNode = join(fakeBin, 'node') + write(fixture.root, 'fake-bin/node', nodeScript) + chmodSync(fakeNode, 0o755) + git(fixture, [ + 'config', + 'merge.dsh-translation-pairing.driver', + `${shellQuote(driverLauncher)} %O %A %B %P`, + ]) + return spawnSync('git', ['-C', fixture.root, 'merge', '--no-commit', 'master'], { + encoding: 'utf8', + env: { + ...fixture.env, + PATH: `${fakeBin}${delimiter}${fixture.env.PATH ?? ''}`, + }, + }) +} + +function createFixture(attributes = true): Fixture { + const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-merge-')) + fixtures.push(root) + const env: NodeJS.ProcessEnv = { + ...process.env, + GIT_AUTHOR_EMAIL: 'pairing@example.test', + GIT_AUTHOR_NAME: 'Pairing Test', + GIT_COMMITTER_EMAIL: 'pairing@example.test', + GIT_COMMITTER_NAME: 'Pairing Test', + GIT_CONFIG_GLOBAL: join(root, 'global.gitconfig'), + GIT_CONFIG_NOSYSTEM: '1', + GIT_DEFAULT_HASH: 'sha1', + } + const fixture = { env, root } + execFileSync('git', ['init', '--quiet', '--initial-branch=master', root], { env }) + if (attributes) write(root, '.gitattributes', '*.i18n.yaml merge=dsh-translation-pairing\n') + return fixture +} + +function record(root: string, path: string, source: string, zh: string): string { + const paths = translationPairPaths(path) + write(root, paths.source, source) + write(root, paths.zh, zh) + const content = renderTranslationPairingRecord(paths, { + sourceHash: storeGitBlob(root, Buffer.from(source)), + zhHash: storeGitBlob(root, Buffer.from(zh)), + }) + write(root, paths.meta, content) + return content +} + +const baseSource = '# Guide\n\nEnglish | [中文](guide.zh.md)\n\nAlpha base.\n\nBeta base.\n' +const baseZh = '# 指南\n\n[English](guide.md) | 中文\n\n甲基础。\n\n乙基础。\n' +const currentSource = baseSource.replace('Alpha base.', 'Alpha current.') +const currentZh = baseZh.replace('甲基础。', '甲当前。') +const otherSource = baseSource.replace('Beta base.', 'Beta other.') +const otherZh = baseZh.replace('乙基础。', '乙对侧。') +const mergedSource = currentSource.replace('Beta base.', 'Beta other.') +const mergedZh = currentZh.replace('乙基础。', '乙对侧。') +const manualBaseSource = baseSource.replace('guide.zh.md', 'manual.zh.md') +const manualBaseZh = baseZh.replace('guide.md', 'manual.md') +const manualCurrentSource = manualBaseSource.replace('Alpha base.', 'Alpha current.') +const manualCurrentZh = manualBaseZh.replace('甲基础。', '甲当前。') +const manualOtherSource = manualBaseSource.replace('Alpha base.', 'Alpha other.') +const manualOtherZh = manualBaseZh.replace('甲基础。', '甲对侧。') + +function commitPair(fixture: Fixture, source: string, zh: string, message: string): string { + const sidecar = record(fixture.root, 'docs/guide.md', source, zh) + git(fixture, ['add', '.']) + git(fixture, ['commit', '-m', message]) + return sidecar +} + +function commitTextCleanPair(fixture: Fixture, source: string, zh: string, message: string): void { + const sidecar = record(fixture.root, 'docs/guide.md', source, zh) + write( + fixture.root, + 'docs/guide.i18n.yaml', + sidecar.replace('\nguide.zh.md:', '\n# Stable separator for independent line merges.\nguide.zh.md:'), + ) + git(fixture, ['add', '.']) + git(fixture, ['commit', '-m', message]) +} + +function createDivergedPair(fixture: Fixture): { ancestor: string; current: string; other: string } { + const ancestor = commitPair(fixture, baseSource, baseZh, 'base') + git(fixture, ['switch', '-c', 'current']) + const current = commitPair(fixture, currentSource, currentZh, 'current') + git(fixture, ['switch', 'master']) + const other = commitPair(fixture, otherSource, otherZh, 'other') + git(fixture, ['switch', 'current']) + return { ancestor, current, other } +} + +function createTextCleanDivergedPair(fixture: Fixture): void { + commitTextCleanPair(fixture, baseSource, baseZh, 'base') + git(fixture, ['switch', '-c', 'current']) + commitTextCleanPair(fixture, currentSource, baseZh, 'current source') + git(fixture, ['switch', 'master']) + commitTextCleanPair(fixture, baseSource, otherZh, 'other translation') + git(fixture, ['switch', 'current']) +} + +function startStoppedPairingMerge(fixture: Fixture): void { + createDivergedPair(fixture) + const merge = spawnSync('git', ['-C', fixture.root, 'merge', '--no-commit', 'master'], { + encoding: 'utf8', + env: fixture.env, + }) + expect(merge.status).toBe(1) + expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('docs/guide.i18n.yaml') +} + +function commitMixedPairs( + fixture: Fixture, + guide: { source: string; zh: string }, + manual: { source: string; zh: string }, + message: string, +): void { + record(fixture.root, 'docs/guide.md', guide.source, guide.zh) + record(fixture.root, 'docs/manual.md', manual.source, manual.zh) + git(fixture, ['add', '.']) + git(fixture, ['commit', '-m', message]) +} + +function startMixedPairingMerge(fixture: Fixture): void { + commitMixedPairs( + fixture, + { source: baseSource, zh: baseZh }, + { source: manualBaseSource, zh: manualBaseZh }, + 'base', + ) + git(fixture, ['switch', '-c', 'current']) + commitMixedPairs( + fixture, + { source: currentSource, zh: currentZh }, + { source: manualCurrentSource, zh: manualCurrentZh }, + 'current', + ) + git(fixture, ['switch', 'master']) + commitMixedPairs( + fixture, + { source: otherSource, zh: otherZh }, + { source: manualOtherSource, zh: manualOtherZh }, + 'other', + ) + git(fixture, ['switch', 'current']) + const merge = spawnSync('git', ['-C', fixture.root, 'merge', '--no-commit', 'master'], { + encoding: 'utf8', + env: fixture.env, + }) + expect(merge.status).toBe(1) +} + +function expectMergedPair(fixture: Fixture): void { + expect(readFileSync(join(fixture.root, 'docs/guide.md'), 'utf8')).toBe(mergedSource) + expect(readFileSync(join(fixture.root, 'docs/guide.zh.md'), 'utf8')).toBe(mergedZh) + expect(readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8')).toBe( + renderTranslationPairingRecord(translationPairPaths('docs/guide.md'), { + sourceHash: gitBlobHash(Buffer.from(mergedSource)), + zhHash: gitBlobHash(Buffer.from(mergedZh)), + }), + ) +} + +describe('translation pairing merge composition', () => { + it('rejects a pairing-record path outside the repository', () => { + const fixture = createFixture(false) + + expect(() => mergeTranslationPairingRecords( + fixture.root, + '../guide.i18n.yaml', + '', + '', + '', + )).toThrow('pairing record escapes the repository') + }) + + it('merges the owner blobs named by three valid records', () => { + const fixture = createFixture(false) + git(fixture, ['config', 'merge.default', 'text']) + const records = createDivergedPair(fixture) + + const result = mergeTranslationPairingRecords( + fixture.root, + 'docs/guide.i18n.yaml', + records.ancestor, + records.current, + records.other, + ) + + expect(result.sourceContent.toString('utf8')).toBe(mergedSource) + expect(result.zhContent.toString('utf8')).toBe(mergedZh) + expect(result.sourceHash).toBe(gitBlobHash(Buffer.from(mergedSource))) + expect(result.zhHash).toBe(gitBlobHash(Buffer.from(mergedZh))) + }) + + it('leaves owner-content conflicts for a human', () => { + const fixture = createFixture(false) + const ancestor = record(fixture.root, 'docs/guide.md', baseSource, baseZh) + const current = record( + fixture.root, + 'docs/guide.md', + baseSource.replace('Alpha base.', 'Alpha current.'), + baseZh.replace('甲基础。', '甲当前。'), + ) + const other = record( + fixture.root, + 'docs/guide.md', + baseSource.replace('Alpha base.', 'Alpha other.'), + baseZh.replace('甲基础。', '甲对侧。'), + ) + + expect(() => mergeTranslationPairingRecords( + fixture.root, + 'docs/guide.i18n.yaml', + ancestor, + current, + other, + )).toThrow('docs/guide.md has content conflicts') + }) + + it('rejects structurally divergent clean owner merges', () => { + const fixture = createFixture(false) + const ancestor = record(fixture.root, 'docs/guide.md', baseSource, baseZh) + const current = record(fixture.root, 'docs/guide.md', currentSource, currentZh) + const other = record( + fixture.root, + 'docs/guide.md', + `${otherSource}\n## Extra\n`, + otherZh, + ) + + expect(() => mergeTranslationPairingRecords( + fixture.root, + 'docs/guide.i18n.yaml', + ancestor, + current, + other, + )).toThrow('clean merges diverge structurally') + }) + + it('refuses owners assigned to another merge strategy', () => { + const fixture = createFixture(false) + write(fixture.root, '.gitattributes', 'docs/*.md merge=custom-owner\n') + const records = createDivergedPair(fixture) + + expect(() => mergeTranslationPairingRecords( + fixture.root, + 'docs/guide.i18n.yaml', + records.ancestor, + records.current, + records.other, + )).toThrow('docs/guide.md uses merge=custom-owner') + }) + + it('refuses unspecified owners affected by merge.default', () => { + const fixture = createFixture(false) + git(fixture, ['config', 'merge.default', 'custom-owner']) + const records = createDivergedPair(fixture) + + expect(() => mergeTranslationPairingRecords( + fixture.root, + 'docs/guide.i18n.yaml', + records.ancestor, + records.current, + records.other, + )).toThrow('merge.default=custom-owner') + }) + + it('runs as Git\'s custom driver and commits a clean composed record', () => { + const fixture = createFixture() + createDivergedPair(fixture) + installFixtureRuntime(fixture.root) + git(fixture, [ + 'config', + 'merge.dsh-translation-pairing.driver', + 'scripts/merge-translation-pairing-driver.sh %O %A %B %P', + ]) + + git(fixture, ['merge', '--no-edit', 'master']) + + expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('') + expectMergedPair(fixture) + }) + + it('leaves an ordinary recoverable conflict when the configured runtime is unavailable', () => { + const fixture = createFixture() + const records = createDivergedPair(fixture) + const headBefore = git(fixture, ['rev-parse', 'HEAD']) + + const result = startMergeWithFakeNode(fixture) + + expect(result.status).toBe(1) + expect(result.stderr).toContain('runtime is unavailable; leaving an ordinary text conflict') + expect(git(fixture, ['rev-parse', 'HEAD'])).toBe(headBefore) + expect(git(fixture, ['rev-parse', '--verify', 'MERGE_HEAD'])).not.toBe('') + expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('docs/guide.i18n.yaml') + expect(git(fixture, ['ls-files', '--unmerged', '--', 'docs/guide.i18n.yaml']).split('\n')).toHaveLength(3) + const conflicted = readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8') + expect(conflicted).toContain('<<<<<<< docs/guide.i18n.yaml:current') + for (const record of [records.current, records.other]) { + const dataLines = record.split('\n').filter(line => line !== '' && !line.startsWith('#')).join('\n') + expect(conflicted).toContain(dataLines) + } + + expect(resolveTranslationPairingConflicts(fixture.root)).toEqual(['docs/guide.i18n.yaml']) + expectMergedPair(fixture) + }) + + it('falls back before a broken driver entrypoint can replace the launcher', () => { + const fixture = createFixture() + createDivergedPair(fixture) + const result = startMergeWithFakeNode( + fixture, + '#!/bin/sh\nif [ "$3" = "--eval" ]; then exit 0; fi\nexit 72\n', + ) + + expect(result.status).toBe(1) + expect(result.stderr).toContain('runtime is unavailable; leaving an ordinary text conflict') + expect(readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8')).toContain( + '<<<<<<< docs/guide.i18n.yaml:current', + ) + }) + + it('keeps a clean text fallback unresolved until the explicit resolver confirms it', () => { + const fixture = createFixture() + createTextCleanDivergedPair(fixture) + const result = startMergeWithFakeNode(fixture) + + expect(result.status).toBe(1) + expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('docs/guide.i18n.yaml') + const canonicalRecord = renderTranslationPairingRecord(translationPairPaths('docs/guide.md'), { + sourceHash: gitBlobHash(Buffer.from(currentSource)), + zhHash: gitBlobHash(Buffer.from(otherZh)), + }) + expect(readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8')).toBe( + canonicalRecord.replace( + '\nguide.zh.md:', + '\n# Stable separator for independent line merges.\nguide.zh.md:', + ), + ) + + expect(resolveTranslationPairingConflicts(fixture.root)).toEqual(['docs/guide.i18n.yaml']) + expect(readFileSync(join(fixture.root, 'docs/guide.md'), 'utf8')).toBe(currentSource) + expect(readFileSync(join(fixture.root, 'docs/guide.zh.md'), 'utf8')).toBe(otherZh) + expect(readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8')).toBe(canonicalRecord) + }) + + it('leaves a staged merge when the pre-merge-commit hook rejects it', () => { + const fixture = createFixture() + createDivergedPair(fixture) + installFixtureRuntime(fixture.root) + git(fixture, [ + 'config', + 'merge.dsh-translation-pairing.driver', + 'scripts/merge-translation-pairing-driver.sh %O %A %B %P', + ]) + const hooks = join(fixture.root, 'hooks') + write( + fixture.root, + 'hooks/pre-merge-commit', + '#!/bin/sh\necho "fixture pre-merge-commit rejection" >&2\nexit 77\n', + ) + chmodSync(join(hooks, 'pre-merge-commit'), 0o755) + git(fixture, ['config', 'core.hooksPath', hooks]) + const headBefore = git(fixture, ['rev-parse', 'HEAD']) + + const result = spawnSync('git', ['-C', fixture.root, 'merge', '--no-edit', 'master'], { + encoding: 'utf8', + env: fixture.env, + }) + + expect(result.status).toBe(1) + expect(result.stderr).toContain('fixture pre-merge-commit rejection') + expect(git(fixture, ['rev-parse', 'HEAD'])).toBe(headBefore) + expect(git(fixture, ['rev-parse', '--verify', 'MERGE_HEAD'])).not.toBe('') + expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('') + expect(git(fixture, ['diff', '--cached', '--name-only']).split('\n')).toContain( + 'docs/guide.i18n.yaml', + ) + expectMergedPair(fixture) + }) + + it('prints the recovery path when driver input is not composable', () => { + const fixture = createFixture(false) + const result = spawnSync(process.execPath, ['--import', tsxLoader, driver], { + cwd: fixture.root, + encoding: 'utf8', + env: fixture.env, + }) + + expect(result.status).toBe(1) + expect(result.stderr).toContain('pnpm run verify-translation-pairing --write ') + expect(result.stderr).toContain('pnpm run resolve-translation-pairing-conflicts') + }) + + it('resolves an already-stopped generated-only conflict from index stages', () => { + const fixture = createFixture(false) + startStoppedPairingMerge(fixture) + + expect(resolveTranslationPairingConflicts(fixture.root)).toEqual(['docs/guide.i18n.yaml']) + + expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('') + expectMergedPair(fixture) + }) + + it('refuses to confirm unstaged owner bytes after a stopped merge', () => { + const fixture = createFixture(false) + startStoppedPairingMerge(fixture) + write(fixture.root, 'docs/guide.md', `${mergedSource}\nunstaged\n`) + + expect(() => resolveTranslationPairingConflicts(fixture.root)).toThrow( + 'docs/guide.md has unstaged content', + ) + expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('docs/guide.i18n.yaml') + }) + + it('refuses to overwrite an edited sidecar after a stopped merge', () => { + const fixture = createFixture(false) + startStoppedPairingMerge(fixture) + write(fixture.root, 'docs/guide.i18n.yaml', 'manually resolved\n') + + expect(() => resolveTranslationPairingConflicts(fixture.root)).toThrow( + 'docs/guide.i18n.yaml has edited conflict content', + ) + expect(readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8')).toBe('manually resolved\n') + expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('docs/guide.i18n.yaml') + }) + + it('resolves safe records while leaving an owner-conflicted pair untouched', () => { + const fixture = createFixture(false) + startMixedPairingMerge(fixture) + + expect(() => resolveTranslationPairingConflicts(fixture.root)).toThrow( + 'docs/manual.i18n.yaml: docs/manual.md has content conflicts', + ) + + expect(git(fixture, ['diff', '--name-only', '--diff-filter=U']).split('\n')).toEqual([ + 'docs/manual.i18n.yaml', + 'docs/manual.md', + 'docs/manual.zh.md', + ]) + expectMergedPair(fixture) + }) +}) diff --git a/scripts/translation-pairing-merge.ts b/scripts/translation-pairing-merge.ts new file mode 100644 index 0000000000..7c6d38469c --- /dev/null +++ b/scripts/translation-pairing-merge.ts @@ -0,0 +1,338 @@ +/** Fail-closed composition of bilingual pairing records during Git merges. */ + +import { spawnSync } from 'node:child_process' +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path' +import { + GIT_COMMAND_MAX_BUFFER, + gitBlobHash, + readGitIndexBlob, + runGit, + storeGitBlob, +} from './translation-pairing-git.ts' +import { + linksTo, + isTranslationScopeFile, + parseTranslationMarkdown, + translationStructureDiff, + translationStructureSignature, +} from './translation-pairing.ts' +import { + parseTranslationPairingRecord, + renderTranslationPairingRecord, + translationPairPathsFromMeta, + type TranslationPairPaths, + type TranslationPairingRecord, +} from './translation-pairing-record.ts' + +const UNMERGED_ENTRY = /^(\d+) ([0-9a-f]+) ([123])\t([\s\S]+)$/ + +/** A mechanically composed record and the exact merged owner contents it names. */ +export interface TranslationPairingMergeResult extends TranslationPairingRecord { + /** Canonical generated sidecar text. */ + record: string + /** Clean three-way merge of the English owner. */ + sourceContent: Buffer + /** Clean three-way merge of the Simplified Chinese owner. */ + zhContent: Buffer +} + +interface UnmergedStages { + ancestor?: string + current?: string + other?: string +} + +function readGitBlob(root: string, objectId: string, owner: string): Buffer { + const content = runGit(root, ['cat-file', 'blob', objectId], `reading ${owner} blob ${objectId}`) + if (gitBlobHash(content) !== objectId) { + throw new Error(`${owner} record names ${objectId}, which is not its SHA-1 git blob hash`) + } + return content +} + +function readMergeDefault(root: string): string | undefined { + const result = spawnSync('git', ['-C', root, 'config', '--get', 'merge.default'], { + maxBuffer: GIT_COMMAND_MAX_BUFFER, + }) + if (result.error) { + throw new Error(`reading merge.default failed: ${result.error.message}`, { cause: result.error }) + } + if (result.status === 1) return undefined + if (result.status !== 0) { + throw new Error( + `reading merge.default failed with status ${String(result.status)}: ${result.stderr.toString('utf8').trim()}`, + ) + } + return result.stdout.toString('utf8').trim() +} + +function assertDefaultTextMerge(root: string, paths: TranslationPairPaths): void { + const output = runGit( + root, + ['check-attr', '-z', 'merge', '--', paths.source, paths.zh], + 'checking bilingual owner merge attributes', + ).toString('utf8') + const fields = output.split('\0') + fields.pop() + let mergeDefault: string | undefined + for (let index = 0; index < fields.length; index += 3) { + const path = fields[index] + const value = fields[index + 2] + if (path === undefined || value === undefined) { + throw new Error('git check-attr returned a malformed result') + } + if (!['unspecified', 'set', 'text'].includes(value)) { + throw new Error(`${path} uses merge=${value}; the pairing driver only composes Git's default text merge`) + } + if (value === 'unspecified') { + mergeDefault ??= readMergeDefault(root) + if (mergeDefault !== undefined && mergeDefault !== 'text') { + throw new Error( + `${path} inherits merge.default=${mergeDefault}; the pairing driver only composes Git's default text merge`, + ) + } + } + } +} + +function runTextMerge( + root: string, + label: string, + ancestor: Buffer | string, + current: Buffer | string, + other: Buffer | string, +): { output: Buffer; status: number | null } { + const temporary = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-merge-')) + try { + const ancestorPath = join(temporary, 'ancestor') + const currentPath = join(temporary, 'current') + const otherPath = join(temporary, 'other') + writeFileSync(ancestorPath, ancestor) + writeFileSync(currentPath, current) + writeFileSync(otherPath, other) + const result = spawnSync('git', [ + '-C', root, + 'merge-file', '-p', + '-L', `${label}:current`, + '-L', `${label}:ancestor`, + '-L', `${label}:other`, + currentPath, ancestorPath, otherPath, + ], { maxBuffer: GIT_COMMAND_MAX_BUFFER }) + if (result.error) { + throw new Error(`merging ${label} failed: ${result.error.message}`, { cause: result.error }) + } + return { output: result.stdout, status: result.status } + } finally { + rmSync(temporary, { recursive: true, force: true }) + } +} + +function mergeBlobTriplet( + root: string, + owner: string, + ancestor: Buffer, + current: Buffer, + other: Buffer, +): Buffer { + const result = runTextMerge(root, owner, ancestor, current, other) + if (result.status !== 0) { + const kind = result.status !== null && result.status > 0 && result.status <= 127 + ? 'has content conflicts' + : `failed with status ${String(result.status)}` + throw new Error(`${owner} ${kind}`) + } + return result.output +} + +function loadRecordOwners( + root: string, + label: string, + content: string, + paths: TranslationPairPaths, +): { source: Buffer; zh: Buffer } { + const record = parseTranslationPairingRecord(content, paths) + if (record === undefined) throw new Error(`${label} ${paths.meta} is not a valid two-hash pairing record`) + return { + source: readGitBlob(root, record.sourceHash, `${label} ${paths.source}`), + zh: readGitBlob(root, record.zhHash, `${label} ${paths.zh}`), + } +} + +function assertMergedPairStructure(paths: TranslationPairPaths, source: Buffer, zh: Buffer): void { + const sourceTree = parseTranslationMarkdown(source.toString('utf8')) + const zhTree = parseTranslationMarkdown(zh.toString('utf8')) + if (!linksTo(sourceTree, basename(paths.zh))) { + throw new Error(`${paths.source} clean merge lost its language-switcher link to ${basename(paths.zh)}`) + } + if (!linksTo(zhTree, basename(paths.source))) { + throw new Error(`${paths.zh} clean merge lost its language-switcher link to ${basename(paths.source)}`) + } + const divergences = translationStructureDiff( + translationStructureSignature(sourceTree, basename(paths.zh)), + translationStructureSignature(zhTree, basename(paths.source)), + ) + if (divergences.length > 0) { + throw new Error(`${paths.source} and ${paths.zh} clean merges diverge structurally: ${divergences.join('; ')}`) + } +} + +function normalizeMetaPath(root: string, meta: string): string { + if (isAbsolute(meta)) throw new Error(`pairing record must be repository-relative: ${JSON.stringify(meta)}`) + const repositoryRelative = relative(resolve(root), resolve(root, meta)) + if (repositoryRelative === '' || repositoryRelative === '..' || repositoryRelative.startsWith(`..${sep}`)) { + throw new Error(`pairing record escapes the repository: ${JSON.stringify(meta)}`) + } + return repositoryRelative.split(sep).join('/') +} + +/** + * Compose one generated sidecar from the ancestor, current, and other records. + * + * Each input record is already a confirmation of its two owner blobs. The + * result exists only when Git's default text merge succeeds independently for + * both languages and the composed documents retain the pairing structure. + * + * @param root - Repository root containing the referenced Git objects. + * @param metaPath - Repository-relative sidecar path. + * @param ancestorRecord - Common-ancestor sidecar text. + * @param currentRecord - Current-side sidecar text. + * @param otherRecord - Other-side sidecar text. + * @returns The canonical record and exact merged owner contents. + * @throws Error when the input is not mechanically composable. + */ +export function mergeTranslationPairingRecords( + root: string, + metaPath: string, + ancestorRecord: string, + currentRecord: string, + otherRecord: string, +): TranslationPairingMergeResult { + const normalizedMeta = normalizeMetaPath(root, metaPath) + if (!isTranslationScopeFile(normalizedMeta)) { + throw new Error(`${normalizedMeta} is outside the active bilingual documentation corpus`) + } + const paths = translationPairPathsFromMeta(normalizedMeta) + assertDefaultTextMerge(root, paths) + const ancestor = loadRecordOwners(root, 'ancestor', ancestorRecord, paths) + const current = loadRecordOwners(root, 'current', currentRecord, paths) + const other = loadRecordOwners(root, 'other', otherRecord, paths) + const sourceContent = mergeBlobTriplet(root, paths.source, ancestor.source, current.source, other.source) + const zhContent = mergeBlobTriplet(root, paths.zh, ancestor.zh, current.zh, other.zh) + assertMergedPairStructure(paths, sourceContent, zhContent) + const sourceHash = storeGitBlob(root, sourceContent) + const zhHash = storeGitBlob(root, zhContent) + return { + record: renderTranslationPairingRecord(paths, { sourceHash, zhHash }), + sourceContent, + sourceHash, + zhContent, + zhHash, + } +} + +function unmergedSidecars(root: string): Map { + const output = runGit(root, ['ls-files', '--unmerged', '-z'], 'listing unresolved merge entries').toString('utf8') + const records = new Map() + for (const entry of output.split('\0')) { + if (entry === '') continue + const match = UNMERGED_ENTRY.exec(entry) + if (!match?.[2] || !match[3] || match[4] === undefined) { + throw new Error(`git ls-files returned a malformed unmerged entry: ${JSON.stringify(entry)}`) + } + const path = match[4] + if (!path.endsWith('.i18n.yaml')) continue + const stages = records.get(path) ?? {} + const field = match[3] === '1' ? 'ancestor' : match[3] === '2' ? 'current' : 'other' + stages[field] = match[2] + records.set(path, stages) + } + return records +} + +function assertUneditedSidecar( + root: string, + metaPath: string, + ancestorRecord: string, + currentRecord: string, + otherRecord: string, +): void { + const worktreeRecord = readFileSync(join(root, metaPath), 'utf8') + if (worktreeRecord === currentRecord || worktreeRecord === otherRecord) return + const textMerge = runTextMerge(root, metaPath, ancestorRecord, currentRecord, otherRecord) + if (textMerge.status === 0 && textMerge.output.toString('utf8') === worktreeRecord) return + const stageDataLines = [currentRecord, otherRecord] + .flatMap(record => record.split(/\r?\n/)) + .filter(line => line !== '' && !line.startsWith('#')) + const hasUneditedConflict = worktreeRecord.includes('<<<<<<<') + && worktreeRecord.includes('=======') + && worktreeRecord.includes('>>>>>>>') + && stageDataLines.every(line => worktreeRecord.includes(line)) + if (!hasUneditedConflict) { + throw new Error(`${metaPath} has edited conflict content; refusing to overwrite manual work`) + } +} + +/** + * Resolve every mechanically composable `.i18n.yaml` conflict in the index. + * + * The command first proves that Git's already-staged owner merges match the + * independently composed contents, then writes and stages all sidecars as one + * batch. Other conflicts remain untouched; after staging the safe records, an + * aggregate error reports any pairing conflicts that still need manual work. + * + * @param root - Repository root with an in-progress merge-like operation. + * @returns Repository-relative sidecar paths resolved and staged. + */ +export function resolveTranslationPairingConflicts(root: string): string[] { + const resolutions: { path: string; record: string }[] = [] + const failures: { path: string; reason: string }[] = [] + for (const [metaPath, stages] of [...unmergedSidecars(root)].sort(([left], [right]) => left.localeCompare(right))) { + try { + if (stages.ancestor === undefined || stages.current === undefined || stages.other === undefined) { + throw new Error('is an add/delete or incomplete-stage conflict and requires manual resolution') + } + const ancestorRecord = readGitBlob(root, stages.ancestor, `ancestor ${metaPath}`).toString('utf8') + const currentRecord = readGitBlob(root, stages.current, `current ${metaPath}`).toString('utf8') + const otherRecord = readGitBlob(root, stages.other, `other ${metaPath}`).toString('utf8') + assertUneditedSidecar(root, metaPath, ancestorRecord, currentRecord, otherRecord) + const result = mergeTranslationPairingRecords( + root, + metaPath, + ancestorRecord, + currentRecord, + otherRecord, + ) + const paths = translationPairPathsFromMeta(metaPath) + if (readGitIndexBlob(root, paths.source)?.objectId !== result.sourceHash) { + throw new Error(`${paths.source} staged merge does not match the pairing driver's clean merge`) + } + if (readGitIndexBlob(root, paths.zh)?.objectId !== result.zhHash) { + throw new Error(`${paths.zh} staged merge does not match the pairing driver's clean merge`) + } + for (const [path, expected] of [[paths.source, result.sourceHash], [paths.zh, result.zhHash]] as const) { + if (gitBlobHash(readFileSync(join(root, path))) !== expected) { + throw new Error(`${path} has unstaged content; refusing to confirm bytes outside the merge result`) + } + } + resolutions.push({ path: metaPath, record: result.record }) + } catch (error) { + failures.push({ path: metaPath, reason: error instanceof Error ? error.message : String(error) }) + } + } + for (const resolution of resolutions) writeFileSync(join(root, resolution.path), resolution.record) + if (resolutions.length > 0) { + runGit(root, ['add', '--', ...resolutions.map(resolution => resolution.path)], 'staging resolved pairing records') + } + if (failures.length > 0) { + const resolved = resolutions.length === 0 + ? '' + : `resolved and staged ${resolutions.map(resolution => resolution.path).join(', ')}; ` + throw new Error( + `${resolved}left ${String(failures.length)} pairing conflict(s) unresolved:\n` + + failures.map(failure => `- ${failure.path}: ${failure.reason}`).join('\n'), + ) + } + return resolutions.map(resolution => resolution.path) +} diff --git a/scripts/translation-pairing-record.ts b/scripts/translation-pairing-record.ts new file mode 100644 index 0000000000..d4481ea5b4 --- /dev/null +++ b/scripts/translation-pairing-record.ts @@ -0,0 +1,99 @@ +/** Canonical paths, parsing, and rendering for bilingual pairing records. */ + +import { basename } from 'node:path' + +/** The three repository-relative paths that form one bilingual pair. */ +export interface TranslationPairPaths { + /** English document path. */ + source: string + /** Simplified Chinese document path. */ + zh: string + /** Generated consistency-record path. */ + meta: string +} + +/** The two content hashes recorded for a bilingual pair. */ +export interface TranslationPairingRecord { + /** Git blob hash of the English document. */ + sourceHash: string + /** Git blob hash of the Simplified Chinese document. */ + zhHash: string +} + +const META_LINE = /^([^:#]+\.md): ([0-9a-f]{40})$/ + +/** + * Derive the counterpart and consistency-record paths from an English document. + * + * @param source - Repository-relative English Markdown path. + * @returns The complete three-path pair. + */ +export function translationPairPaths(source: string): TranslationPairPaths { + if (!source.endsWith('.md') || source.endsWith('.zh.md')) { + throw new Error(`expected an English Markdown path, received ${JSON.stringify(source)}`) + } + return { + source, + zh: source.replace(/\.md$/, '.zh.md'), + meta: source.replace(/\.md$/, '.i18n.yaml'), + } +} + +/** + * Derive one pair from its consistency-record path. + * + * @param meta - Repository-relative `foo.i18n.yaml` path. + * @returns The complete three-path pair. + */ +export function translationPairPathsFromMeta(meta: string): TranslationPairPaths { + if (!meta.endsWith('.i18n.yaml')) { + throw new Error(`expected a bilingual consistency-record path, received ${JSON.stringify(meta)}`) + } + return translationPairPaths(meta.replace(/\.i18n\.yaml$/, '.md')) +} + +/** + * Parse a consistency record for its expected sibling names. + * + * @param content - Complete sidecar text. + * @param paths - Expected sibling paths. + * @returns The two hashes, or `undefined` for malformed, duplicate, or unexpected keys. + */ +export function parseTranslationPairingRecord( + content: string, + paths: TranslationPairPaths, +): TranslationPairingRecord | undefined { + const hashes = new Map() + for (const line of content.split('\n')) { + if (line === '' || line.startsWith('#')) continue + const match = META_LINE.exec(line) + if (!match?.[1] || !match[2] || hashes.has(match[1])) return undefined + hashes.set(match[1], match[2]) + } + const sourceHash = hashes.get(basename(paths.source)) + const zhHash = hashes.get(basename(paths.zh)) + if (hashes.size !== 2 || sourceHash === undefined || zhHash === undefined) return undefined + return { sourceHash, zhHash } +} + +/** + * Render the canonical consistency record for a pair. + * + * @param paths - Pair paths written into the record and its recovery command. + * @param record - Confirmed content hashes. + * @returns Canonical YAML text with exactly one trailing newline. + */ +export function renderTranslationPairingRecord( + paths: TranslationPairPaths, + record: TranslationPairingRecord, +): string { + return [ + '# 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 ${paths.source}`, + `${basename(paths.source)}: ${record.sourceHash}`, + `${basename(paths.zh)}: ${record.zhHash}`, + '', + ].join('\n') +} 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 4d44266887..b89df38f52 100644 --- a/scripts/translation-pairing.spec.ts +++ b/scripts/translation-pairing.spec.ts @@ -1,17 +1,24 @@ /** Regression tests for bilingual snapshots, corpus scope, and structure. */ import { execFileSync, spawnSync } from 'node:child_process' -import { mkdtempSync, rmSync } from 'node:fs' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it } from 'vitest' -import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts' +import { gitBlobHash, readGitIndexBlob, storeGitBlob } from './translation-pairing-git.ts' import { + parseTranslationPairingRecord, + renderTranslationPairingRecord, + translationPairPaths, +} from './translation-pairing-record.ts' +import { + blobHash, isTranslationScopeFile, pairAnchorOfArgument, parseTranslationMarkdown, parseTranslationPairingCliArgs, parseTranslationPairingManifest, + partitionGeneratedRegions, translationStructureDiff, translationStructureSignature, } from './translation-pairing.ts' @@ -74,6 +81,28 @@ describe('translation pairing snapshots', () => { } }) + it('reads staged bytes independently of the working tree', () => { + const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-index-')) + try { + execFileSync('git', ['init', '--quiet', root], { + env: { ...process.env, GIT_DEFAULT_HASH: 'sha1' }, + }) + execFileSync('git', ['-C', root, 'config', 'user.email', 'pairing@example.test']) + execFileSync('git', ['-C', root, 'config', 'user.name', 'Pairing Test']) + writeFileSync(join(root, 'owner.md'), 'staged') + execFileSync('git', ['-C', root, 'add', 'owner.md']) + writeFileSync(join(root, 'owner.md'), 'unstaged') + + const indexed = readGitIndexBlob(root, 'owner.md') + + expect(indexed?.content.toString('utf8')).toBe('staged') + expect(indexed?.objectId).toBe(gitBlobHash(Buffer.from('staged'))) + expect(readGitIndexBlob(root, 'absent.md')).toBeUndefined() + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) + it.skipIf(!supportsSha256ObjectFormat)('rejects an object format that pairing records cannot represent', () => { const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-')) try { @@ -113,6 +142,32 @@ describe('translation pairing manifest', () => { }) }) +describe('translation pairing records', () => { + const paths = translationPairPaths('docs/foo.md') + const record = { + sourceHash: '1'.repeat(40), + zhHash: '2'.repeat(40), + } + + it('round-trips the canonical two-hash record', () => { + expect(parseTranslationPairingRecord(renderTranslationPairingRecord(paths, record), paths)).toEqual(record) + }) + + it('rejects duplicate or unexpected keys', () => { + expect(parseTranslationPairingRecord([ + `foo.md: ${'1'.repeat(40)}`, + `foo.md: ${'3'.repeat(40)}`, + `foo.zh.md: ${'2'.repeat(40)}`, + '', + ].join('\n'), paths)).toBeUndefined() + expect(parseTranslationPairingRecord([ + `foo.md: ${'1'.repeat(40)}`, + `bar.zh.md: ${'2'.repeat(40)}`, + '', + ].join('\n'), paths)).toBeUndefined() + }) +}) + describe('translation scope discovery', () => { it.each([ 'README.md', @@ -186,28 +241,95 @@ describe('pair CLI arguments', () => { it('scopes a check to named pairs and dedupes the three spellings', () => { expect(parseTranslationPairingCliArgs(['docs/foo.zh.md', 'docs/foo.i18n.yaml', 'docs/bar.md'])).toEqual({ + input: 'worktree', mode: 'check', scope: 'pairs', anchors: ['docs/bar.md', 'docs/foo.md'], }) - expect(parseTranslationPairingCliArgs([])).toEqual({ mode: 'check', scope: 'corpus', anchors: [] }) + expect(parseTranslationPairingCliArgs([])).toEqual({ + input: 'worktree', + mode: 'check', + scope: 'corpus', + anchors: [], + }) }) it('requires --write to name confirmed pairs or opt into --all', () => { expect(() => parseTranslationPairingCliArgs(['--write'])).toThrow('requires the pair(s) you confirmed') expect(parseTranslationPairingCliArgs(['--write', 'docs/foo.md'])).toEqual({ + input: 'worktree', mode: 'write', scope: 'pairs', anchors: ['docs/foo.md'], }) - expect(parseTranslationPairingCliArgs(['--write', '--all'])).toEqual({ mode: 'write', scope: 'corpus', anchors: [] }) + expect(parseTranslationPairingCliArgs(['--write', '--all'])).toEqual({ + input: 'worktree', + mode: 'write', + scope: 'corpus', + anchors: [], + }) expect(() => parseTranslationPairingCliArgs(['--write', '--all', 'docs/foo.md'])).toThrow('not both') }) it('keeps --list corpus-only and rejects unknown flags', () => { - expect(parseTranslationPairingCliArgs(['--list'])).toEqual({ mode: 'list', scope: 'corpus', anchors: [] }) + expect(parseTranslationPairingCliArgs(['--list'])).toEqual({ + input: 'worktree', + mode: 'list', + scope: 'corpus', + anchors: [], + }) expect(() => parseTranslationPairingCliArgs(['--list', 'docs/foo.md'])).toThrow('takes no other flags or paths') expect(() => parseTranslationPairingCliArgs(['--all'])).toThrow('--all only applies to --write') expect(() => parseTranslationPairingCliArgs(['--frobnicate'])).toThrow('unknown flag(s): --frobnicate') }) + + it('makes cached verification a named, read-only index check', () => { + expect(parseTranslationPairingCliArgs(['--cached', 'docs/foo.i18n.yaml'])).toEqual({ + input: 'index', + mode: 'check', + scope: 'pairs', + anchors: ['docs/foo.md'], + }) + expect(() => parseTranslationPairingCliArgs(['--cached'])).toThrow('requires the staged pair paths') + 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 7f24d9bce7..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 = /^