+```
+
+Types: [ContentBlock](llm-streaming.zh.md) · [Scoped](scope.zh.md)
+
+Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
+
#### `tools/result` — emit
diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml
index 569d755993..392f448802 100644
--- a/docs/tool-catalog.i18n.yaml
+++ b/docs/tool-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 docs/tool-catalog.md
-tool-catalog.md: 16142c2f7d98cf1037b034d2836c742b62f26594
-tool-catalog.zh.md: 533caacc7a923b5ea36f527292f420b610f1c488
+tool-catalog.md: 91ff093e79cc05a2c08b5aa1130378440cd963f3
+tool-catalog.zh.md: 35daf5a7c6b1c27975721c0b9d8ddfe71eaeb803
diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md
index 16142c2f7d..91ff093e79 100644
--- a/docs/tool-catalog.md
+++ b/docs/tool-catalog.md
@@ -16,7 +16,7 @@ This table connects model-visible tool names to the plugin package and service s
| Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Deployment note |
| --- | --- | --- | --- | --- | --- |
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. |
-| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch-start + tool/code-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. |
+| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch-start + tool/code-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` / `mode: both` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. |
| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. |
| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.jobs` runtime and is collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled. |
| `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.shell`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.jobs` runtime and are collected/stopped through the `job_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-shell-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables. |
@@ -144,9 +144,9 @@ Execute a TypeScript program against the available tools. Takes two required arg
}
```
-Source: [`packages/core/tools/src/code-mode.ts`](../packages/core/tools/src/code-mode.ts)
+Source: [`packages/core/tools/src/ptc.ts`](../packages/core/tools/src/ptc.ts)
-Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.
+Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` / `mode: both` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.
diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md
index 533caacc7a..35daf5a7c6 100644
--- a/docs/tool-catalog.zh.md
+++ b/docs/tool-catalog.zh.md
@@ -20,7 +20,7 @@
| 工具包 | 模型可见名称 | 依赖 | 写入/影响 | 随产品发布的别名 | 部署说明 |
| --- | --- | --- | --- | --- | --- |
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after a UI/provider answers the question` | - | ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类答案。 |
-| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`、`ctx.codeRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/code-dispatch-start + tool/code-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: code`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 Code Mode Agent Note)。在 `code` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 |
+| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`、`ctx.codeRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/code-dispatch-start + tool/code-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: ptc`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 |
| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 |
| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;禁用 `enableRunInBackground` 配置(默认为 true)后,该参数会被完全移除。 |
| `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费方(由 `@deepseek-ai/dsh-pwsh-local` 等 PowerShell 执行器为 `ctx.shell` 提供后端);除沙箱接口外,它逐项对应 bash 工具调用。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具收集/停止;托管的 `DSH_*` 环境来自 `@deepseek-ai/dsh-shell-env`。每次调用都在新进程中运行,不使用持久 PTY 会话。路径采用原生 `C:\...` 形式,变量采用 `$env:NAME`。 |
@@ -148,9 +148,9 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类
}
```
-来源:[`packages/core/tools/src/code-mode.ts`](../packages/core/tools/src/code-mode.ts)
+来源:[`packages/core/tools/src/ptc.ts`](../packages/core/tools/src/ptc.ts)
-在 `mode: code`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 Code Mode Agent Note)。在 `code` 下,它是注册表对协议格式的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。
+在 `mode: ptc`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。
diff --git a/docs/tool-execution-pipeline.i18n.yaml b/docs/tool-execution-pipeline.i18n.yaml
index bd8de3ba60..6ff5ef0a15 100644
--- a/docs/tool-execution-pipeline.i18n.yaml
+++ b/docs/tool-execution-pipeline.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/tool-execution-pipeline.md
-tool-execution-pipeline.md: d04d2e4e5093fee92f8921f0eb0112c960a81bb8
-tool-execution-pipeline.zh.md: 15627023d3be6ac2b3aae70c2ef01ef9f1077d3e
+tool-execution-pipeline.md: a799c68a60f6782ef3bb79c52f89cbc78d762ab3
+tool-execution-pipeline.zh.md: 7ffdd34298f630c975b23f23daf875a3c4cdc84d
diff --git a/docs/tool-execution-pipeline.md b/docs/tool-execution-pipeline.md
index d04d2e4e50..a799c68a60 100644
--- a/docs/tool-execution-pipeline.md
+++ b/docs/tool-execution-pipeline.md
@@ -57,6 +57,6 @@ flowchart TD
allResults --> context
```
-Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition's snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.
+Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition's snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. PTC mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.
Maintenance mode: curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs.
diff --git a/docs/tool-execution-pipeline.zh.md b/docs/tool-execution-pipeline.zh.md
index 15627023d3..7ffdd34298 100644
--- a/docs/tool-execution-pipeline.zh.md
+++ b/docs/tool-execution-pipeline.zh.md
@@ -59,6 +59,6 @@ flowchart TD
allResults --> context
```
-文件系统的先读后编辑检查位于 `tool-fs` 之下,通过 `fs/*` 事件实现。通用的前置/后置 waterfall 承载钩子与审批策略;`ctx.approval` 在单调守卫之前处理询问,而不得重新排序的所有者策略仍作为已注册的守卫。超时等环绕分发关注点对 `tools/execute` 进行包装。注册表会对候选结果进行无损快照;如果快照失败,则会先将失败规范化,之后再由可见定义中已随快照固定的 `finalizeContent` 回调强制执行其同步且仅限内容的不变式。随后,`tools/result` 会观察不可变、可由 JSON 无损表示的结果。这样一来,钩子便可跨越不同工具系列,而无需让工具与某个策略服务耦合。Code Mode 会将保留的 `run_code` 传输及其序列化子调用都送入流水线;子调用携带父级 token、记录 `tool/code-dispatch`、将拒绝呈现为具有约束力的驳回,并省略 `additionalContexts`,以保持调用与结果相邻。
+文件系统的先读后编辑检查位于 `tool-fs` 之下,通过 `fs/*` 事件实现。通用的前置/后置 waterfall 承载钩子与审批策略;`ctx.approval` 在单调守卫之前处理询问,而不得重新排序的所有者策略仍作为已注册的守卫。超时等环绕分发关注点对 `tools/execute` 进行包装。注册表会对候选结果进行无损快照;如果快照失败,则会先将失败规范化,之后再由可见定义中已随快照固定的 `finalizeContent` 回调强制执行其同步且仅限内容的不变式。随后,`tools/result` 会观察不可变、可由 JSON 无损表示的结果。这样一来,钩子便可跨越不同工具系列,而无需让工具与某个策略服务耦合。PTC mode 会将保留的 `run_code` 传输及其序列化子调用都送入流水线;子调用携带父级 token、记录 `tool/code-dispatch`、将拒绝呈现为具有约束力的驳回,并省略 `additionalContexts`,以保持调用与结果相邻。
维护模式:英文源文件包含人工维护的 Mermaid 流程图,并由生成器写出;本中文文件作为经评审对侧通过双语配对维护。确切的工具 schema 与事件签名位于生成的目录中。
diff --git a/docs/user/develop/basic/tool.i18n.yaml b/docs/user/develop/basic/tool.i18n.yaml
index 99fdfe2139..0c7c5bbc82 100644
--- a/docs/user/develop/basic/tool.i18n.yaml
+++ b/docs/user/develop/basic/tool.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/basic/tool.md
-tool.md: 24a82d277e626ba78760a0afb1c41e2a157eb8ee
-tool.zh.md: a07bef588f5a18093cf2eb1971b855bf40d1d0b6
+tool.md: aeed5cf0e742cdde33bdcb86bfac4de471ae2597
+tool.zh.md: 25538e42b7777fe3b05b361379a8bce70d6ec5f5
diff --git a/docs/user/develop/basic/tool.md b/docs/user/develop/basic/tool.md
index 24a82d277e..aeed5cf0e7 100644
--- a/docs/user/develop/basic/tool.md
+++ b/docs/user/develop/basic/tool.md
@@ -48,5 +48,5 @@ Open `http://127.0.0.1:3080` and ask: `Use the greet tool to greet Ada.` The mod
## Next steps
- [Plugin configuration](./config.md) — make the greeting configurable.
-- [Tool authoring reference](../../../cookbook/adding-a-tool.md) — look up nested schemas, canonical values, background work, policy hooks, Code Mode, and UI cards.
+- [Tool authoring reference](../../../cookbook/adding-a-tool.md) — look up nested schemas, canonical values, background work, policy hooks, PTC mode, and UI cards.
- [Capability layering](../practice/index.md) — split a replaceable capability into Service Definition, Service Provider, and Consumer packages.
diff --git a/docs/user/develop/basic/tool.zh.md b/docs/user/develop/basic/tool.zh.md
index a07bef588f..25538e42b7 100644
--- a/docs/user/develop/basic/tool.zh.md
+++ b/docs/user/develop/basic/tool.zh.md
@@ -48,5 +48,5 @@ pnpm dsh web --patch ./scratch-plugin/cordis.yml
## 下一步
- [插件配置](./config.zh.md) — 让问候语可配置。
-- [工具编写参考](../../../cookbook/adding-a-tool.zh.md) — 查阅嵌套 schema、规范值、后台工作、策略钩子、Code Mode 和 UI 卡片。
+- [工具编写参考](../../../cookbook/adding-a-tool.zh.md) — 查阅嵌套 schema、规范值、后台工作、策略钩子、PTC mode 和 UI 卡片。
- [能力分层](../practice/index.zh.md) — 将可替换能力拆分为 Service Definition、Service Provider 和 Consumer 三类包。
diff --git a/knip.json b/knip.json
deleted file mode 100644
index a1c247f0d5..0000000000
--- a/knip.json
+++ /dev/null
@@ -1,779 +0,0 @@
-{
- "$schema": "https://unpkg.com/knip@5/schema.json",
- "exclude": [
- "duplicates"
- ],
- "ignoreBinaries": [
- "bwrap",
- "icacls",
- "musl-gcc",
- "python3",
- "sandbox-exec",
- "tar",
- "taskkill",
- "where.exe"
- ],
- "ignoreWorkspaces": [
- "vendor/*",
- "python/sdk-runtime"
- ],
- "ignoreDependencies": [
- "@yarnpkg/cli-dist",
- "lightningcss"
- ],
- "workspaces": {
- ".": {
- "entry": [
- "scripts/**/*.mjs",
- "scripts/**/*.cjs",
- "snapshots/**/*.mjs",
- "scripts/types/client-build-environment/index.d.ts"
- ],
- "ignoreUnresolved": [
- "client-build-environment"
- ],
- "project": [
- "scripts/**/*.ts",
- "scripts/**/*.mjs",
- "scripts/**/*.cjs",
- "snapshots/**/*.mjs"
- ]
- },
- "packages/host/directory-picker-auto": {
- "ignoreDependencies": [
- "@deepseek-ai/dsh-client-ui-directory-picker-browse",
- "@deepseek-ai/dsh-client-ui-directory-picker-native"
- ]
- },
- "packages/experimental/inspector": {
- "entry": [
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/extensions/cordis-host-runner": {
- "entry": [
- "tests/**/*.spec.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "zod"
- ]
- },
- "packages/host/directory-picker-native": {
- "entry": [
- "tests/**/*.spec.{ts,tsx}",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.{ts,tsx}",
- "tests/**/*.{ts,tsx}"
- ]
- },
- "packages/api/remotes": {
- "entry": [
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/client/ui-approval": {
- "entry": [
- "tests/**/*.spec.tsx"
- ],
- "project": [
- "src/**/*.{ts,tsx}",
- "tests/**/*.tsx"
- ]
- },
- "packages/client/ui-conversation": {
- "entry": [
- "tests/**/*.spec.{ts,tsx}",
- "tests/**/*.perf.client.ts"
- ],
- "project": [
- "src/**/*.{ts,tsx}",
- "tests/**/*.{ts,tsx}"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/dsh-client-ui-workspace"
- ]
- },
- "packages/client/ui-sidebar": {
- "ignoreDependencies": [
- "@deepseek-ai/dsh-client-ui-workspace"
- ]
- },
- "packages/client/ui-subagent": {
- "ignoreDependencies": [
- "@deepseek-ai/dsh-client-ui-input-trigger"
- ]
- },
- "packages/client/ui-primitives": {
- "entry": [
- "tests/**/*.spec.tsx"
- ],
- "project": [
- "src/**/*.ts",
- "src/**/*.tsx",
- "tests/**/*.tsx"
- ]
- },
- "packages/client/ui-goal": {
- "entry": [
- "tests/**/*.spec.tsx"
- ],
- "project": [
- "src/**/*.ts",
- "src/**/*.tsx",
- "tests/**/*.tsx"
- ]
- },
- "packages/client/ui-directory-picker-browse": {
- "entry": [
- "tests/**/*.spec.tsx"
- ],
- "project": [
- "src/**/*.ts",
- "src/**/*.tsx",
- "tests/**/*.tsx"
- ]
- },
- "packages/client/ui-directory-picker-native": {
- "entry": [
- "tests/**/*.spec.tsx"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.tsx"
- ]
- },
- "packages/client/ui-deliverables": {
- "entry": [
- "tests/**/*.spec.tsx"
- ],
- "project": [
- "src/**/*.ts",
- "src/**/*.tsx",
- "tests/**/*.tsx"
- ]
- },
- "packages/client/ui-workflow-run": {
- "entry": [
- "tests/**/*.spec.tsx"
- ],
- "project": [
- "src/**/*.ts",
- "src/**/*.tsx",
- "tests/**/*.tsx"
- ]
- },
- "packages/client/ui-layout": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.spec.tsx"
- ],
- "project": [
- "src/**/*.ts",
- "src/**/*.tsx",
- "tests/**/*.ts",
- "tests/**/*.tsx"
- ]
- },
- "website": {
- "project": [
- "**/*.ts"
- ],
- "ignoreDependencies": [
- "@braintree/sanitize-url",
- "cytoscape",
- "cytoscape-cose-bilkent",
- "dayjs",
- "debug"
- ]
- },
- "packages/*/*": {
- "entry": [
- "tests/**/*.spec.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/experimental/webworker-runtime": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/compile/transform-corpus-check.ts",
- "tests/fixtures/vfs-example/workspace/src/preview.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "buffer",
- "@deepseek-ai/dsh-client-modules"
- ]
- },
- "packages/typert/generator": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/fixtures/type-model/**/*.ts",
- "tests/fixtures/remote-model/**/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/shell/bash-sandbox": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/shell/pwsh-sandbox": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/e2b/e2b": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/fixtures/composition/bin.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/context/time-context": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/fixtures/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/dsh-agent-spine-demo",
- "@deepseek-ai/dsh-bash-local",
- "@deepseek-ai/dsh-session-checkpoint-policy",
- "@deepseek-ai/dsh-session-persistence-jsonl",
- "@deepseek-ai/dsh-subprocess-local"
- ]
- },
- "packages/lsp/lsp-stdio": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/fixture-server.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "typescript-language-server"
- ]
- },
- "packages/sandbox/sandbox-local": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/session/session-telemetry-otel": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/fixtures/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/cordis-plugin-logger-console",
- "@deepseek-ai/dsh-agent-spine-demo",
- "@deepseek-ai/dsh-bash-local",
- "@deepseek-ai/dsh-session-checkpoint-policy",
- "@deepseek-ai/dsh-session-persistence-jsonl",
- "@deepseek-ai/dsh-subprocess-local"
- ]
- },
- "packages/util/brand": {
- "project": [
- "src/**/*.ts"
- ]
- },
- "packages/attachment/attachment": {
- "project": [
- "src/**/*.ts"
- ]
- },
- "packages/test-support/session-snapshot": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/fixtures/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/test-support/loader-smoke": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/fixtures/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/core/agent-loop": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/goal/goal": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/fixtures/domain/seed-goal.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/dsh-agent-spine-demo",
- "@deepseek-ai/dsh-bash-local",
- "@deepseek-ai/dsh-session-checkpoint-policy",
- "@deepseek-ai/dsh-session-persistence-jsonl",
- "@deepseek-ai/dsh-subprocess-local"
- ]
- },
- "packages/session-query/session-query-sqlite": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/code-runtime/code-runtime-worker-thread": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/code-runtime/code-runtime-python": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/llm/llm": {
- "ignoreDependencies": [
- "zod"
- ]
- },
- "packages/llm/llm-deepseek": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/llm/llm-pi-ai": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/session/session-title-first-prompt-llm": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/context/agent-instructions": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/context/session-reference": {
- "ignoreDependencies": [
- "zod"
- ]
- },
- "packages/session/session-checkpoint-policy": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/web/web-search-exa": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/web/web-search-perplexity": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/workflow/workflow-worker-thread": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/web/web-search-deepseek": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/examples/agent-spine-demo": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/sdk/server": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/interaction/commands": {
- "entry": [
- "tests/**/*.spec.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "zod"
- ]
- },
- "packages/subagent/subagent-spawn-in-process": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/subagent/subagent-acp": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/mock-acp-server.ts",
- "tests/fixtures/loader/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/dsh-agent-spine-demo",
- "@deepseek-ai/dsh-session-checkpoint-policy",
- "@deepseek-ai/dsh-session-persistence-jsonl",
- "@deepseek-ai/dsh-tool-subagent"
- ]
- },
- "packages/subagent/subagent-codex": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/fixtures/loader/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/dsh-agent-spine-demo",
- "@deepseek-ai/dsh-session-checkpoint-policy",
- "@deepseek-ai/dsh-session-persistence-jsonl",
- "@deepseek-ai/dsh-tool-subagent"
- ]
- },
- "packages/subagent/subagent-claude-code": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/fixtures/loader/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/dsh-agent-spine-demo",
- "@deepseek-ai/dsh-session-checkpoint-policy",
- "@deepseek-ai/dsh-session-persistence-jsonl",
- "@deepseek-ai/dsh-subagent-codex",
- "@deepseek-ai/dsh-tool-subagent"
- ]
- },
- "packages/fs/tool-fs": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/mcp/mcp-client": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/fixture-server.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@modelcontextprotocol/server-everything",
- "@modelcontextprotocol/server-filesystem"
- ]
- },
- "packages/client/web": {
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/client/ui-renderer": {
- "entry": [
- "tests/**/*.spec.tsx"
- ],
- "project": [
- "src/**/*.ts",
- "src/**/*.tsx",
- "tests/**/*.tsx"
- ]
- },
- "apps/web": {
- "entry": [
- "tests/**/*.e2e.ts",
- "tests/**/*.perf.ts",
- "tests/**/*.snapshot.ts",
- "tests/support.ts",
- "src/node-module-stub.ts",
- "src/preview.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/dsh-client-store",
- "@deepseek-ai/dsh-client-ui-primitives",
- "@deepseek-ai/dsh-client-ui-slots",
- "@types/react",
- "@types/react-dom",
- "react",
- "react-dom"
- ]
- },
- "apps/cli": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/profiles/**/fixtures/**/*.{ts,mjs}",
- "tests/profiles/**/tests/fixtures/**/*.{ts,mjs}"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/.+"
- ]
- },
- "packages/subagent/subagent-dsh-sdk": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts",
- "tests/fixtures/loader/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@deepseek-ai/dsh-agent-instructions",
- "@deepseek-ai/dsh-agent-spine-demo",
- "@deepseek-ai/dsh-llm-deepseek",
- "@deepseek-ai/dsh-session-checkpoint-policy",
- "@deepseek-ai/dsh-session-persistence-jsonl",
- "@deepseek-ai/dsh-skill-filesystem"
- ]
- },
- "packages/shell/tool-pwsh": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/fixtures/loader/driver.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/test-support/loader-smoke": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/fixtures/*.ts"
- ],
- "project": [
- "src/**/*.ts",
- "tests/**/*.ts"
- ]
- },
- "packages/experimental/agent-team-profile": {
- "ignoreDependencies": [
- "@deepseek-ai/dsh-experimental-agent-team",
- "@deepseek-ai/dsh-experimental-tool-agent-team"
- ]
- },
- "packages/bundle/base": {
- "ignoreDependencies": [
- "@deepseek-ai/.+"
- ]
- },
- "packages/bundle/headless": {
- "ignoreDependencies": [
- "@deepseek-ai/dsh-code-runtime-worker-thread"
- ]
- },
- "packages/bundle/acp-app": {
- "ignoreDependencies": [
- "@deepseek-ai/dsh-acp"
- ]
- },
- "packages/bundle/sdk-app": {
- "ignoreDependencies": [
- "@deepseek-ai/dsh-sdk-jsonrpc-server"
- ]
- },
- "packages/bundle/sdk-minimal": {
- "ignoreDependencies": [
- "@deepseek-ai/.+"
- ]
- },
- "packages/experimental/agent-team": {
- "entry": [
- "tests/**/*.spec.ts",
- "tests/**/*.e2e.ts"
- ]
- },
- "packages/experimental/agent-team-web-profile": {
- "ignoreDependencies": [
- "@deepseek-ai/dsh-experimental-client-ui-agent-team"
- ]
- },
- "packages/bundle/web-app": {
- "ignoreDependencies": [
- "@deepseek-ai/.+"
- ]
- }
- }
-}
diff --git a/package.json b/package.json
index e6d9253314..9587f220d0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-root",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"license": "MIT",
"private": true,
"type": "module",
@@ -65,7 +65,6 @@
"check:ci:windows-complete": "tsx scripts/run-gates.ts ci-windows-complete",
"check:ci:windows-observational": "tsx scripts/run-gates.ts ci-windows-observational",
"check:node-compat": "tsx scripts/run-gates.ts node-compat",
- "knip": "knip --treat-config-hints-as-errors",
"publint": "tsx scripts/publint-all.ts",
"doc-typecheck": "npm run build:lib:host && npm run doc-typecheck:contracts-ready",
"doc-typecheck:contracts-ready": "tsx scripts/doc-typecheck.ts",
@@ -148,7 +147,7 @@
"release:verify-packed-install": "tsx scripts/release/verify-packed-install.ts",
"release:publish": "tsx scripts/release/publish.ts",
"dsh": "node --import tsx/esm apps/cli/src/bin.ts",
- "demo:code-mode": "node scripts/demo-code-mode.mjs",
+ "demo:ptc": "node scripts/demo-ptc.mjs",
"demo:inspector": "node --import tsx/esm apps/cli/src/bin.ts web --patch ./packages/experimental/inspector/cordis.source.patch.yml",
"mock:llm": "node --import tsx packages/test-support/llm-mock-server/src/bin.ts",
"dev:web": "tsx scripts/dev-web.ts --poll",
@@ -174,7 +173,6 @@
"js-yaml": "^4.2.0",
"jscpd": "^5.0.12",
"jsdom": "29.1.1",
- "knip": "^6.16.1",
"lefthook": "^2.1.9",
"lightningcss": "^1.32.0",
"mdast-util-from-markdown": "^2.0.3",
diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml
index 7816391ff8..158a9ead5c 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: c3dcbd3032e3eadad21b640f3a614ef5a1a6c6e7
-README.zh.md: 80518ef0de7c2022db45aefa411f507870cf09aa
+README.md: e1a729a6c80d8f8125e0d4c4b038dc631edd7a26
+README.zh.md: 754ad0fc44677afb243c3a32a0281f58ec3b3af2
diff --git a/packages/README.md b/packages/README.md
index c3dcbd3032..e1a729a6c8 100644
--- a/packages/README.md
+++ b/packages/README.md
@@ -40,7 +40,7 @@ Every package lives in exactly one group; new packages join existing groups, and
| [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition + local process-tree provider |
| [`shell/`](shell/README.md) | Bash capability family: executor seam, local impl, model-facing tools |
| [`terminal/`](terminal/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, model-facing tools |
-| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: Service Definition + worker-thread provider + Code Mode Consumer |
+| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: Service Definition + worker-thread provider + PTC mode Consumer |
| [`sandbox/`](sandbox/README.md) | Process-confinement seam; bwrap/Landlock/Seatbelt backends |
| [`fs/`](fs/README.md) | Filesystem capability family: seam, local impl, model-facing file tools, discovery tools |
| [`lsp/`](lsp/README.md) | LSP capability family: seam, generic stdio provider, and the `lsp` tool |
diff --git a/packages/README.zh.md b/packages/README.zh.md
index 80518ef0de..754ad0fc44 100644
--- a/packages/README.zh.md
+++ b/packages/README.zh.md
@@ -40,7 +40,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
| [`subprocess/`](subprocess/README.zh.md) | 子进程能力系列:Service Definition + 本地进程树提供方 |
| [`shell/`](shell/README.zh.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 |
| [`terminal/`](terminal/README.zh.md) | 持久 PTY 能力系列:限定所有者范围的会话、本地实现、面向模型的工具 |
-| [`code-runtime/`](code-runtime/README.zh.md) | 代码执行能力系列:Service Definition + worker 线程提供方 + Code Mode Consumer |
+| [`code-runtime/`](code-runtime/README.zh.md) | 代码执行能力系列:Service Definition + worker 线程提供方 + PTC mode Consumer |
| [`sandbox/`](sandbox/README.zh.md) | 进程限制 seam;bwrap/Landlock/Seatbelt 后端 |
| [`fs/`](fs/README.zh.md) | 文件系统能力系列:seam、本地实现、面向模型的文件工具、发现工具 |
| [`lsp/`](lsp/README.zh.md) | LSP 能力系列:seam、通用 stdio 提供方和 `lsp` 工具 |
diff --git a/packages/acp/acp/package.json b/packages/acp/acp/package.json
index 1bd6af44c1..0e56f2a481 100644
--- a/packages/acp/acp/package.json
+++ b/packages/acp/acp/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-acp",
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -66,6 +66,7 @@
"@deepseek-ai/dsh-token-meter": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/acp/acp/tests/approval.spec.ts b/packages/acp/acp/tests/approval.spec.ts
index 22f1973b77..99cd3695fc 100644
--- a/packages/acp/acp/tests/approval.spec.ts
+++ b/packages/acp/acp/tests/approval.spec.ts
@@ -69,7 +69,11 @@ describe('ACP machine permission policy', () => {
harness = await makeBridgeHarness()
const request = await ownedRequest()
const foreign = {
- session: { id: request.agent.session.id, events: [{ type: 'turn/start' }], append: () => ({}) },
+ session: {
+ id: request.agent.session.id,
+ events: [{ type: 'turn/start', seq: 0, time: 0, data: { turn: 1 } }],
+ append: () => ({}),
+ },
} as unknown as Agent
await expect(harness.ctx.approval.request({ agent: foreign, toolName: 'bash', callId: ToolCallId('call') }))
.resolves.toBe('unavailable')
diff --git a/packages/acp/acp/tests/harness.ts b/packages/acp/acp/tests/harness.ts
index 70e31cbbe4..39b2d7e030 100644
--- a/packages/acp/acp/tests/harness.ts
+++ b/packages/acp/acp/tests/harness.ts
@@ -23,6 +23,7 @@ import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, St
import { type GenerateOptions, LlmAdapter, ReasoningEffortId, type LlmResolvedModelInfo, type StreamChunk } from '@deepseek-ai/dsh-llm'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import * as AcpPlugin from '../src/index.ts'
@@ -231,6 +232,10 @@ export async function makeBridgeHarness(options: {
const ownsPersistenceRoot = options.persistenceRoot === undefined
const persistenceRoot = options.persistenceRoot ?? await mkdtemp(join(tmpdir(), 'dsh-acp-test-'))
await mountAgentLoopTestDependencies(ctx, { systemPrompt: { persona: options.persona ?? '' } })
+ // The agent loop and the composed approval/permission services declare
+ // sessionProjections a required injection: mount the registry (and with it
+ // the loop's turnBoundary unit) before the loop activates.
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(JsonlSessionPersistence, { root: persistenceRoot, compression: 'none' })
await ctx.plugin(TokenMeter)
if (options.attachments !== false) await ctx.plugin(MemoryAttachmentStore)
diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json
index 13c7e402ae..d5601ab188 100644
--- a/packages/api/gateway/package.json
+++ b/packages/api/gateway/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-gateway",
"description": "Typert Remote Host dispatcher and Client API endpoint",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json
index aef2bcc642..8a271fe29a 100644
--- a/packages/api/remotes/package.json
+++ b/packages/api/remotes/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-remotes",
"description": "Remote BFF assembly for application-selected Host capabilities",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/api/remotes/tests/built-lib.e2e.ts b/packages/api/remotes/tests/built-lib.e2e.ts
index ef7b4ae0ed..fd95221825 100644
--- a/packages/api/remotes/tests/built-lib.e2e.ts
+++ b/packages/api/remotes/tests/built-lib.e2e.ts
@@ -26,6 +26,7 @@ const requiredArtifacts = [
'packages/api/gateway/lib/index.js',
'packages/typert/registry/lib/client.js',
'packages/typert/registry/lib/index.js',
+ 'packages/session/session-projection/lib/index.js',
].every(path => existsSync(artifact(path)))
describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
@@ -42,6 +43,7 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
registryHost: 'packages/typert/registry/lib/index.js',
remotesClient: 'packages/api/remotes/lib/client.js',
session: 'packages/core/session/lib/index.js',
+ sessionProjections: 'packages/session/session-projection/lib/index.js',
}).map(([key, path]) => [key, artifactUrl(path)]))
const script = `
import { createServer } from 'node:http'
@@ -53,6 +55,7 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
const connectionHost = await import(urls.connectionHost)
const { default: TypertRemoteService } = await import(urls.apiGatewayHost)
const { default: GoalService } = await import(urls.goal)
+ const { default: SessionProjectionRegistry } = await import(urls.sessionProjections)
const { TYPERT } = await import(urls.goalTypert)
const { default: TypertRegistry } = await import(urls.registryHost)
const { Session, SessionId } = await import(urls.session)
@@ -81,6 +84,7 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
await host.plugin(TypertRegistry)
await host.plugin(AgentRegistry)
await host.plugin(TypertRemoteService)
+ await host.plugin(SessionProjectionRegistry)
await host.plugin(GoalService)
host.typert.register(TYPERT)
diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json
index 191ad25909..0cdd53a2a3 100644
--- a/packages/api/session-controller/package.json
+++ b/packages/api/session-controller/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-session-controller",
"description": "Session Remote commands, cold reads, and live control transport",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -133,6 +133,9 @@
"@deepseek-ai/dsh-session-query": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-session-title": "workspace:^",
+ "@deepseek-ai/dsh-storage": "workspace:^",
+ "@deepseek-ai/dsh-storage-domain": "workspace:^",
+ "@deepseek-ai/dsh-storage-json": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
diff --git a/packages/api/session-controller/tests/session-projections.host.spec.ts b/packages/api/session-controller/tests/session-projections.host.spec.ts
index fd30f66158..43391af21f 100644
--- a/packages/api/session-controller/tests/session-projections.host.spec.ts
+++ b/packages/api/session-controller/tests/session-projections.host.spec.ts
@@ -8,6 +8,9 @@
*/
import { describe, expect, it, vi } from 'vitest'
+import { mkdtemp, readFile, rm } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { z } from 'zod'
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
@@ -19,6 +22,10 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
+import SessionProjectionCache, { projectionCacheDomainSpec } from '@deepseek-ai/dsh-session-projection-cache'
+import Storage from '@deepseek-ai/dsh-storage'
+import * as StorageDomain from '@deepseek-ai/dsh-storage-domain'
+import * as StorageJson from '@deepseek-ai/dsh-storage-json'
import { SessionControlController } from '@deepseek-ai/dsh-api-session-controller/src/control.ts'
import type { SessionControlFrame, SessionFollowFrame } from '@deepseek-ai/dsh-api-session-controller/types'
import { createSessionTestRemote, type TestSessionRemote } from './test-remote.ts'
@@ -27,6 +34,7 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionStateMap {
'test/last-user': LastUserState
'test/internal-count': number
+ 'test/private-prompt': string | null
}
interface SessionProjectionMap {
'test/last-user': { text: string } | null
@@ -91,6 +99,16 @@ const internalCountUnit = () => ({
stateVersion: 1,
}) satisfies ProjectionDefinition<'test/internal-count', number>
+const privatePromptUnit = () => ({
+ key: 'test/private-prompt',
+ stateSchema: z.string().nullable(),
+ init: () => null,
+ apply: (state, event) => (event.type === 'user/message'
+ ? (event.data.content[0] as { text?: string }).text ?? ''
+ : state),
+ stateVersion: 1,
+}) satisfies ProjectionDefinition<'test/private-prompt', string | null>
+
async function harness(withRegistry: boolean): Promise<{ ctx: Context; session: Session }> {
const ctx = new Context()
await ctx.plugin(SessionStore)
@@ -420,6 +438,60 @@ describe('session.list projections column', () => {
})
})
+ it('keeps persisted host-only state out of a cold session.list response', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'dsh-api-projcache-'))
+ const ctx = new Context()
+ try {
+ await ctx.plugin(Storage)
+ await ctx.plugin(StorageJson, { root })
+ await ctx.plugin(StorageDomain, { backend: 'json' })
+ await ctx.plugin(SessionStore)
+ await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
+ ctx.sessionProjections.register(privatePromptUnit())
+ await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
+ const gateway = remote(ctx)
+ await new Promise(resolve => setTimeout(resolve, 0))
+
+ const id = SessionId('session-cold-host-state')
+ const secret = 'private prompt text from the cache'
+ let session: Session | undefined
+ const owner = await ctx.plugin(Object.assign((sessionCtx: Context) => {
+ session = sessionCtx.sessions.create(id, { meta: { createdAt: 5, cwd: '/workspace' } })
+ }, { inject: ['sessions'] }))
+ if (session === undefined) throw new Error('session was not created')
+ session.append('turn/start', { turn: 1 })
+ session.append('user/message', createUserMessage({
+ content: [{ type: 'text', text: secret }],
+ source: { kind: 'user' },
+ }), { surfaceOp: 'append' })
+ await ctx.sessionProjectionCache.write(session)
+ const stored = await readFile(
+ join(root, projectionCacheDomainSpec.name, 'sessions', `${id}.json`),
+ 'utf8',
+ )
+ expect(stored).toContain(secret)
+
+ const header = session.header
+ await owner.dispose()
+ expect(ctx.sessions.get(id)).toBeUndefined()
+ ctx.provide('sessionPersistence', {
+ list: async () => [header],
+ locate: () => undefined,
+ } as never)
+
+ const response = await gateway.list(request({}))
+ if (!response.ok) throw new Error('unreachable')
+ const row = response.value.items.find(item => item.sessionId === id)
+ expect(row?.projections?.values.sessionListMetadata).toMatchObject({ blank: false })
+ expect('test/private-prompt' in (row?.projections?.values ?? {})).toBe(false)
+ expect(JSON.stringify(row)).not.toContain(secret)
+ } finally {
+ await ctx.fiber.dispose()
+ await rm(root, { recursive: true, force: true })
+ }
+ })
+
it('cold rows without a cache plugin (or without a stored row) just lack the column', async () => {
const { ctx } = await harness(true)
const coldId = SessionId('session-cold-uncached')
diff --git a/packages/api/session-controller/tests/session-rename.host.spec.ts b/packages/api/session-controller/tests/session-rename.host.spec.ts
index b73e72d0e3..b1c54602e5 100644
--- a/packages/api/session-controller/tests/session-rename.host.spec.ts
+++ b/packages/api/session-controller/tests/session-rename.host.spec.ts
@@ -10,6 +10,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import SessionStore from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
@@ -26,6 +27,7 @@ function request(payload: P): P {
async function composed(withTitles = true): Promise {
const ctx = new Context()
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentRegistry)
if (withTitles) {
await ctx.plugin(SessionTitleService, { fallbackMaxWords: 5, fallbackMaxBytes: 40, maxTitleBytes: 40 })
diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json
index 4de5f8d490..3f3039aa68 100644
--- a/packages/api/settings-controller/package.json
+++ b/packages/api/settings-controller/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-settings-controller",
"description": "Remote owner for the configuration surfaces over the settings-domain seams",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json
index bc045f38c7..14586d912e 100644
--- a/packages/api/workspace-controller/package.json
+++ b/packages/api/workspace-controller/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-workspace-controller",
"description": "Workspace Remote commands and reconnect-safe state transport",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/attachment/attachment-local/package.json b/packages/attachment/attachment-local/package.json
index 194112be29..550a44efd6 100644
--- a/packages/attachment/attachment-local/package.json
+++ b/packages/attachment/attachment-local/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment-local",
"description": "Private content-addressed DSH_HOME attachment storage",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/attachment/attachment/package.json b/packages/attachment/attachment/package.json
index 1abd03e3e0..d2448330bc 100644
--- a/packages/attachment/attachment/package.json
+++ b/packages/attachment/attachment/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment",
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json
index d33cae8870..bd03b913cd 100644
--- a/packages/boot/app-boot/package.json
+++ b/packages/boot/app-boot/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-app-boot",
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/boot/cmdline/package.json b/packages/boot/cmdline/package.json
index 60c63d4ec4..83c272e666 100644
--- a/packages/boot/cmdline/package.json
+++ b/packages/boot/cmdline/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-cmdline",
"description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/bundle/acp-app/package.json b/packages/bundle/acp-app/package.json
index 5113761d29..4262b34412 100644
--- a/packages/bundle/acp-app/package.json
+++ b/packages/bundle/acp-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-acp-app",
"description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json
index 82dcc1e007..daef0bab99 100644
--- a/packages/bundle/base/package.json
+++ b/packages/bundle/base/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-base",
"description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/bundle/headless/README.i18n.yaml b/packages/bundle/headless/README.i18n.yaml
index fc0be1d54b..0aba9ff549 100644
--- a/packages/bundle/headless/README.i18n.yaml
+++ b/packages/bundle/headless/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/headless/README.md
-README.md: 952ae369e58b60389a951f2f48a080d735a3d9ad
-README.zh.md: 8d9084550fd295c19b679b64cf17f0e2cdafbeb6
+README.md: 282fa76dd720751d61ba7f44e5e35884d5f21e8f
+README.zh.md: 5a2727a25e6e4840900ebc9dc70d10ff13db12dd
diff --git a/packages/bundle/headless/README.md b/packages/bundle/headless/README.md
index 952ae369e5..282fa76dd7 100644
--- a/packages/bundle/headless/README.md
+++ b/packages/bundle/headless/README.md
@@ -65,7 +65,7 @@ The runner awaits the complete application (`ctx.get('loader')?.await()`) so the
### Patch surface over base
-The patch rides over `dsh-base`: it inherits the projection cache, sets the coding persona on the base `system-prompt` row, keeps the same temporary process-wide Code Mode opt-in (`DSH_TOOLS_MODE`) as the Web surface, disables the shared HMR row, inserts Code Mode's worker as a core execution capability, and mounts the startup provider and the runner. The cache checkpoints each persisted one-shot session for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. The startup provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), reads the positional argument, prints the app's `--help`, and provides `headlessStartup`; the runner injects that service and reads its task from lazy config.
+The patch rides over `dsh-base`: it inherits the projection cache, sets the coding persona on the base `system-prompt` row, keeps the same temporary process-wide PTC mode opt-in (`DSH_TOOLS_MODE`) as the Web surface, disables the shared HMR row, inserts PTC mode's worker as a core execution capability, and mounts the startup provider and the runner. The cache checkpoints each persisted one-shot session for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. The startup provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), reads the positional argument, prints the app's `--help`, and provides `headlessStartup`; the runner injects that service and reads its task from lazy config.
### Exit mapping
diff --git a/packages/bundle/headless/README.zh.md b/packages/bundle/headless/README.zh.md
index 8d9084550f..5a2727a25e 100644
--- a/packages/bundle/headless/README.zh.md
+++ b/packages/bundle/headless/README.zh.md
@@ -65,7 +65,7 @@ runner 等待整个应用结算(`ctx.get('loader')?.await()`),确保已组
### 叠加在 base 之上的 patch 表层
-patch 叠加在 `dsh-base` 之上:继承投影缓存,在基础 `system-prompt` 行上设置编码 persona,保留与 Web 表层相同的临时进程级 Code Mode 开关(`DSH_TOOLS_MODE`),禁用共享的 HMR 行,把 Code Mode 的 worker 作为核心执行能力插入,并挂载启动提供方与 runner。缓存为每个已持久化的一次性会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 行。启动提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.zh.md)),读取位置参数、打印应用自己的 `--help`,并提供 `headlessStartup`;runner 注入该服务,再从惰性配置中读取任务。
+patch 叠加在 `dsh-base` 之上:继承投影缓存,在基础 `system-prompt` 行上设置编码 persona,保留与 Web 表层相同的临时进程级 PTC mode 开关(`DSH_TOOLS_MODE`),禁用共享的 HMR 行,把 PTC mode 的 worker 作为核心执行能力插入,并挂载启动提供方与 runner。缓存为每个已持久化的一次性会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 行。启动提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.zh.md)),读取位置参数、打印应用自己的 `--help`,并提供 `headlessStartup`;runner 注入该服务,再从惰性配置中读取任务。
### 退出映射
diff --git a/packages/bundle/headless/cordis.patch.yml b/packages/bundle/headless/cordis.patch.yml
index 80cc07be60..d1246b79ba 100644
--- a/packages/bundle/headless/cordis.patch.yml
+++ b/packages/bundle/headless/cordis.patch.yml
@@ -11,11 +11,11 @@
- id: tools
config:
- # Keep the same temporary process-wide Code Mode opt-in as the Web surface.
+ # Keep the same temporary process-wide PTC mode opt-in as the Web surface.
mode: !!js process.env.DSH_TOOLS_MODE
- insert:
- # Code Mode is a core execution capability, not a Web component.
+ # PTC mode is a core execution capability, not a Web component.
- id: code-runtime
name: '@deepseek-ai/dsh-code-runtime-worker-thread'
diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json
index d8c97032c1..474dc74dd6 100644
--- a/packages/bundle/headless/package.json
+++ b/packages/bundle/headless/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-headless",
"description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json
index 6ab4167d8d..5e02cda63e 100644
--- a/packages/bundle/sdk-app/package.json
+++ b/packages/bundle/sdk-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-sdk-app",
"description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml
index 361e9a6f53..0d0c1ff974 100644
--- a/packages/bundle/sdk-minimal/cordis.patch.yml
+++ b/packages/bundle/sdk-minimal/cordis.patch.yml
@@ -33,6 +33,11 @@
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
+ # Shared projection registry: sandbox-policy and terminal-bash fold
+ # sandbox-mode state through its units and require it as a hard injection.
+ - id: session-projection
+ name: '@deepseek-ai/dsh-session-projection'
+
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json
index 42940a0ac8..a0048190db 100644
--- a/packages/bundle/sdk-minimal/package.json
+++ b/packages/bundle/sdk-minimal/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-sdk-minimal",
"description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, editor, and JSONL sessions",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -50,6 +50,7 @@
"@deepseek-ai/dsh-sdk-jsonrpc-server": "workspace:^",
"@deepseek-ai/dsh-session-log-deepseek": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-terminal": "workspace:^",
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
index 7983a7793c..f202ecfaa6 100644
--- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
+++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
@@ -29,6 +29,7 @@ describe('dsh-sdk-minimal bundle', () => {
['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'],
['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek'],
['sandbox', '@deepseek-ai/dsh-sandbox-local'],
+ ['session-projection', '@deepseek-ai/dsh-session-projection'],
['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'],
['subprocess', '@deepseek-ai/dsh-subprocess-local'],
['pty', '@deepseek-ai/dsh-terminal'],
diff --git a/packages/bundle/web-app/README.i18n.yaml b/packages/bundle/web-app/README.i18n.yaml
index 237686c71a..4f438e7d45 100644
--- a/packages/bundle/web-app/README.i18n.yaml
+++ b/packages/bundle/web-app/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/web-app/README.md
-README.md: ed2c341c3eb5b3a5ad2b298c4babd0fc19a066e2
-README.zh.md: 1f57a0beff91fc341bef6f9c6e79dab4f1d1b564
+README.md: 5588c7e72e6d61454620075c239ea571d026c5cf
+README.zh.md: bfd7a58bf5343ddd06b49b97b622493f389da72f
diff --git a/packages/bundle/web-app/README.md b/packages/bundle/web-app/README.md
index ed2c341c3e..5588c7e72e 100644
--- a/packages/bundle/web-app/README.md
+++ b/packages/bundle/web-app/README.md
@@ -73,7 +73,7 @@ The bundle is one patch plus one runtime glue plugin. The storage stack and proj
### Patch semantics
-A patch replaces the targeted row's whole `config`, so each web row restates every key it owns: the persona, the `DSH_TOOLS_MODE` Code Mode opt-in, and the `session-query-sqlite` values on the base rows, then `insert` adds the web host rows, transport, and browser roster. The per-agent tool rows the base mounts process-wide are disabled here and the preset roster takes over; the reasoning for each host-plane versus preset-plane decision is inline in the patch.
+A patch replaces the targeted row's whole `config`, so each web row restates every key it owns: the persona, the `DSH_TOOLS_MODE` PTC mode opt-in, and the `session-query-sqlite` values on the base rows, then `insert` adds the web host rows, transport, and browser roster. The per-agent tool rows the base mounts process-wide are disabled here and the preset roster takes over; the reasoning for each host-plane versus preset-plane decision is inline in the patch.
### Readiness
diff --git a/packages/bundle/web-app/README.zh.md b/packages/bundle/web-app/README.zh.md
index 1f57a0beff..bfd7a58bf5 100644
--- a/packages/bundle/web-app/README.zh.md
+++ b/packages/bundle/web-app/README.zh.md
@@ -73,7 +73,7 @@ dsh --profile web --no-open --port 8080
### patch 语义
-patch 会替换目标行的整个 `config`,因此每个 Web 行都重述自己拥有的每个键:基础行上的 persona、`DSH_TOOLS_MODE` Code Mode 开关与 `session-query-sqlite` 值,随后 `insert` 添加 Web 宿主行、传输层与浏览器名录。base 以进程级挂载的按 agent 工具行在这里被禁用,由 preset 名录接管;每项宿主层与 preset 层归属决策的理由以行内注释写在 patch 里。
+patch 会替换目标行的整个 `config`,因此每个 Web 行都重述自己拥有的每个键:基础行上的 persona、`DSH_TOOLS_MODE` PTC mode 开关与 `session-query-sqlite` 值,随后 `insert` 添加 Web 宿主行、传输层与浏览器名录。base 以进程级挂载的按 agent 工具行在这里被禁用,由 preset 名录接管;每项宿主层与 preset 层归属决策的理由以行内注释写在 patch 里。
### 就绪宣告
diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml
index f63af2885a..9ee698044e 100644
--- a/packages/bundle/web-app/cordis.patch.yml
+++ b/packages/bundle/web-app/cordis.patch.yml
@@ -30,8 +30,8 @@
- id: tools
config:
- # TEMPORARY workaround: DSH_TOOLS_MODE (native|code|both) opts a whole dsh
- # process into Code Mode while per-session tool-presentation selection is being
+ # TEMPORARY workaround: DSH_TOOLS_MODE (native|ptc|both) opts a whole dsh
+ # process into PTC mode while per-session tool-presentation selection is being
# designed; unset keeps the schema default (native). Remove the env seam
# once the web UI owns the choice per session.
mode: !!js process.env.DSH_TOOLS_MODE
diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json
index 85a61a1d5b..a018658347 100644
--- a/packages/bundle/web-app/package.json
+++ b/packages/bundle/web-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-web-app",
"description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json
index 07a00ab2de..f19f30e40d 100644
--- a/packages/client/connection/package.json
+++ b/packages/client/connection/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-connection",
"description": "Authenticated RPC transport, generation lifecycle, and browser fixture",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/hmr/package.json b/packages/client/hmr/package.json
index 3ad65222f5..35f1beed3c 100644
--- a/packages/client/hmr/package.json
+++ b/packages/client/hmr/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-hmr",
"description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json
index 355ba7000e..466734cc9b 100644
--- a/packages/client/locale/package.json
+++ b/packages/client/locale/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-locale",
"description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/modules/package.json b/packages/client/modules/package.json
index 62926fa35b..1e608f4c58 100644
--- a/packages/client/modules/package.json
+++ b/packages/client/modules/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-modules",
"description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/store/package.json b/packages/client/store/package.json
index f2d04490ba..e6b8eb47e6 100644
--- a/packages/client/store/package.json
+++ b/packages/client/store/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-store",
"description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-agent-preset/package.json b/packages/client/ui-agent-preset/package.json
index 60a91df206..7ec49fd96f 100644
--- a/packages/client/ui-agent-preset/package.json
+++ b/packages/client/ui-agent-preset/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-agent-preset",
"description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-agent-preset/src/client/locales.ts b/packages/client/ui-agent-preset/src/client/locales.ts
index 50d7650703..b8e9f8b6e2 100644
--- a/packages/client/ui-agent-preset/src/client/locales.ts
+++ b/packages/client/ui-agent-preset/src/client/locales.ts
@@ -5,7 +5,7 @@ export type AgentPresetSettingsKey =
| 'title' | 'description' | 'loading' | 'error' | 'userTrust' | 'seatHint' | 'headerHint'
| 'nav' | 'sectionIntro' | 'builtIn' | 'setDefault' | 'view'
| 'presetStandardName' | 'presetStandardDescription'
- | 'presetCodeName' | 'presetCodeDescription'
+ | 'presetPtcName' | 'presetPtcDescription'
| 'presetMinimalName' | 'presetMinimalDescription'
| 'presetCordisName' | 'presetCordisDescription'
| 'duplicate' | 'duplicateUnavailable' | 'delete' | 'presetId' | 'presetIdPlaceholder' | 'copyOf'
@@ -37,9 +37,9 @@ export const en: Record = {
presetStandardName: 'Standard mode',
presetStandardDescription:
'Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.',
- presetCodeName: 'PTC mode',
- presetCodeDescription:
- 'All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program.',
+ presetPtcName: 'PTC mode',
+ presetPtcDescription:
+ 'All Standard mode capabilities, with tools exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program.',
presetMinimalName: 'Minimal mode',
presetMinimalDescription:
'Two-tool coding agent with persistent bash and str_replace_editor.',
@@ -101,8 +101,8 @@ export const zh: Record = {
view: '查看',
presetStandardName: '标准模式',
presetStandardDescription: '功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。',
- presetCodeName: 'PTC 模式',
- presetCodeDescription: '具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。',
+ presetPtcName: 'PTC 模式',
+ presetPtcDescription: '具备标准模式的全部能力,并通过 PTC 模式 SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。',
presetMinimalName: '极简模式',
presetMinimalDescription: '仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。',
presetCordisName: '创造模式',
@@ -170,7 +170,7 @@ interface PresetLocaleKeys {
const BUILT_IN_PRESET_KEYS: Readonly>> = {
standard: { name: 'presetStandardName', description: 'presetStandardDescription' },
- code: { name: 'presetCodeName', description: 'presetCodeDescription' },
+ ptc: { name: 'presetPtcName', description: 'presetPtcDescription' },
minimal: { name: 'presetMinimalName', description: 'presetMinimalDescription' },
cordis: { name: 'presetCordisName', description: 'presetCordisDescription' },
}
diff --git a/packages/client/ui-agent-preset/tests/locales.client.spec.ts b/packages/client/ui-agent-preset/tests/locales.client.spec.ts
index 02623e7d7b..1d8b40cc67 100644
--- a/packages/client/ui-agent-preset/tests/locales.client.spec.ts
+++ b/packages/client/ui-agent-preset/tests/locales.client.spec.ts
@@ -8,7 +8,7 @@ const translate = (bundle: typeof en) => (key: keyof typeof en): string => bundl
describe('preset display copy', () => {
it.each([
['standard', 'presetStandardName', 'presetStandardDescription'],
- ['code', 'presetCodeName', 'presetCodeDescription'],
+ ['ptc', 'presetPtcName', 'presetPtcDescription'],
['minimal', 'presetMinimalName', 'presetMinimalDescription'],
['cordis', 'presetCordisName', 'presetCordisDescription'],
] as const)('localizes the shipped %s preset in English and Chinese', (id, nameKey, descriptionKey) => {
diff --git a/packages/client/ui-approval/package.json b/packages/client/ui-approval/package.json
index 90f61b4893..060cb0f386 100644
--- a/packages/client/ui-approval/package.json
+++ b/packages/client/ui-approval/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-approval",
"description": "Approval composer takeover over the scoped Remote Event waterfall",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json
index d5a10b446b..68efaafcd5 100644
--- a/packages/client/ui-attachment/package.json
+++ b/packages/client/ui-attachment/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-attachment",
"description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json
index 33180942fe..b40b6d8664 100644
--- a/packages/client/ui-brand-official/package.json
+++ b/packages/client/ui-brand-official/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-brand-official",
"description": "Official DeepSeek Harness brand occupants for the Web client's sidebar and conversation Hero slots",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json
index 10382a63bf..992ea08102 100644
--- a/packages/client/ui-chat/package.json
+++ b/packages/client/ui-chat/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-chat",
"description": "Chat Conversation target, node definitions, renderers, and details surface",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json
index 07c5c18a52..a5a15451f2 100644
--- a/packages/client/ui-commands/package.json
+++ b/packages/client/ui-commands/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-commands",
"description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json
index c86c5f335e..e9e434071b 100644
--- a/packages/client/ui-conversation/package.json
+++ b/packages/client/ui-conversation/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-conversation",
"description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json
index 15dce11815..a977a5e609 100644
--- a/packages/client/ui-deliverables/package.json
+++ b/packages/client/ui-deliverables/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-deliverables",
"description": "Produced-files turn tail and clickable final-response file references for Web",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-directory-picker-browse/package.json b/packages/client/ui-directory-picker-browse/package.json
index 2d0003c1e8..90cd0ce667 100644
--- a/packages/client/ui-directory-picker-browse/package.json
+++ b/packages/client/ui-directory-picker-browse/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-directory-picker-browse",
"description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-directory-picker-native/package.json b/packages/client/ui-directory-picker-native/package.json
index 986e38be54..9bb352e7cb 100644
--- a/packages/client/ui-directory-picker-native/package.json
+++ b/packages/client/ui-directory-picker-native/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-directory-picker-native",
"description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the host's OS chooser",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-goal/package.json b/packages/client/ui-goal/package.json
index a14e25110e..1bd3457b73 100644
--- a/packages/client/ui-goal/package.json
+++ b/packages/client/ui-goal/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-goal",
"description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json
index d87637ed67..6114813aa3 100644
--- a/packages/client/ui-input-trigger/package.json
+++ b/packages/client/ui-input-trigger/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-input-trigger",
"description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-jobs/package.json b/packages/client/ui-jobs/package.json
index a195c70e08..e944070fe6 100644
--- a/packages/client/ui-jobs/package.json
+++ b/packages/client/ui-jobs/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-jobs",
"description": "Session-header background-job list: live registry state mirrored from session/jobs frames",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json
index d07ae0d563..ea634dbafa 100644
--- a/packages/client/ui-layout/package.json
+++ b/packages/client/ui-layout/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-layout",
"description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-message-feedback/package.json b/packages/client/ui-message-feedback/package.json
index 19df358147..51ca031762 100644
--- a/packages/client/ui-message-feedback/package.json
+++ b/packages/client/ui-message-feedback/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-message-feedback",
"description": "Per-message feedback controls contributed to the assistant-message action strip, backed by the messageFeedback Host Remote",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json
index d14dcd894d..e368cfb6a9 100644
--- a/packages/client/ui-model-selection/package.json
+++ b/packages/client/ui-model-selection/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-model-selection",
"description": "Model selection over the shared model catalog, Session projection, and session.selectModel",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-permission-presets/package.json b/packages/client/ui-permission-presets/package.json
index 037c6d15bd..3531416e93 100644
--- a/packages/client/ui-permission-presets/package.json
+++ b/packages/client/ui-permission-presets/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-permission-presets",
"description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-plan/package.json b/packages/client/ui-plan/package.json
index 8c3f87f136..97cd4f4ca4 100644
--- a/packages/client/ui-plan/package.json
+++ b/packages/client/ui-plan/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-plan",
"description": "Plan-mode composer control: the conversation.input.plan seat over the plan projection and the /plan command channel",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json
index 9410edae05..8041b7dbf3 100644
--- a/packages/client/ui-primitives/package.json
+++ b/packages/client/ui-primitives/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-primitives",
"description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-reference/package.json b/packages/client/ui-reference/package.json
index f7e9afbde5..70928a652c 100644
--- a/packages/client/ui-reference/package.json
+++ b/packages/client/ui-reference/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-reference",
"description": "Unified Web @file and @session reference source",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-renderer/package.json b/packages/client/ui-renderer/package.json
index 88729acada..997ac47de6 100644
--- a/packages/client/ui-renderer/package.json
+++ b/packages/client/ui-renderer/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-renderer",
"description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-session/package.json b/packages/client/ui-session/package.json
index 34087f6979..7ab9428e32 100644
--- a/packages/client/ui-session/package.json
+++ b/packages/client/ui-session/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-session",
"description": "Session Controller adapter for React and session-scoped slots",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json
index 441433fadd..3f4de05dd4 100644
--- a/packages/client/ui-settings-general/package.json
+++ b/packages/client/ui-settings-general/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings-general",
"description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-settings-models/package.json b/packages/client/ui-settings-models/package.json
index 57fa2abcc6..8c65aacc8f 100644
--- a/packages/client/ui-settings-models/package.json
+++ b/packages/client/ui-settings-models/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings-models",
"description": "Models settings and shared product-onboarding dialogs over existing settings and credential joins",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-settings-plugin-inventory/package.json b/packages/client/ui-settings-plugin-inventory/package.json
index 270e353444..1049448e57 100644
--- a/packages/client/ui-settings-plugin-inventory/package.json
+++ b/packages/client/ui-settings-plugin-inventory/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings-plugin-inventory",
"description": "Read-only Cordis Loader inventory tab in Web Plugins settings",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-settings-plugins/package.json b/packages/client/ui-settings-plugins/package.json
index 7edd06ff4e..c0cdbaf6b7 100644
--- a/packages/client/ui-settings-plugins/package.json
+++ b/packages/client/ui-settings-plugins/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings-plugins",
"description": "Plugins settings section with feature-owned tabs and configurable host-plane plugin cards",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-settings/package.json b/packages/client/ui-settings/package.json
index 94d9e4c6d7..6c5bdc2be3 100644
--- a/packages/client/ui-settings/package.json
+++ b/packages/client/ui-settings/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings",
"description": "Settings domain base plugin: the settings-namespace scope service and the canonical settings slot-type contract",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json
index 50ccb7ed46..783a743df5 100644
--- a/packages/client/ui-sidebar/package.json
+++ b/packages/client/ui-sidebar/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-sidebar",
"description": "Sidebar plugin: session multi-level tree, search, grouping, state dots",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-skill/package.json b/packages/client/ui-skill/package.json
index d90bd9fbb2..d6e679509b 100644
--- a/packages/client/ui-skill/package.json
+++ b/packages/client/ui-skill/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-skill",
"description": "Web skill references and the dedicated skill tool row",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-slots/package.json b/packages/client/ui-slots/package.json
index d93b9addc9..0e317a33ee 100644
--- a/packages/client/ui-slots/package.json
+++ b/packages/client/ui-slots/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-slots",
"description": "Slot registry pure core: SlotMap declaration merging, single register composition API, four-share props types, store-seat types, renderer install seam",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-subagent/package.json b/packages/client/ui-subagent/package.json
index 5aeeb4189b..75afb63a17 100644
--- a/packages/client/ui-subagent/package.json
+++ b/packages/client/ui-subagent/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-subagent",
"description": "Subagent conversation catalog, continuation routing UI, and '@' reference source",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json
index 4bb37da9e4..ee343d5a89 100644
--- a/packages/client/ui-theme/package.json
+++ b/packages/client/ui-theme/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-theme",
"description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml
index 11f131475f..5cfd56493f 100644
--- a/packages/client/ui-tool/README.i18n.yaml
+++ b/packages/client/ui-tool/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/ui-tool/README.md
-README.md: 792a70b7b3cd2d7a48187e872104bffac1d54202
-README.zh.md: 0feeba622cd4bcee1d5affff252e7596b7190338
+README.md: 773a93801ebc214e2d5c94d52864f5c5dd887100
+README.zh.md: 88df08d7b5b7d5d3978d90fd4df4cbbb2efeb1fa
diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md
index 792a70b7b3..773a93801e 100644
--- a/packages/client/ui-tool/README.md
+++ b/packages/client/ui-tool/README.md
@@ -96,7 +96,7 @@ None; this package neither assembles nor sends a provider request.
These limits define the dispatch depth and the view ownership; they are current package constraints.
-- **The Host excludes `run_code` from Code Mode program bindings** — production events produce one dispatch level; the recursive Runtime/UI contract supports nesting.
+- **The Host excludes `run_code` from PTC mode program bindings** — production events produce one dispatch level; the recursive Runtime/UI contract supports nesting.
- **First-party Tool views are colocated here** — they can move to their owning business packages independently through the keyed slot.
- **Tool copy reuses the `ui-conversation` locale namespace** — tool titles, row chrome, and Cordis-free primitive labels use that dictionary; presenter models retain locale keys or data rather than rendered wording.
diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md
index 0feeba622c..88df08d7b5 100644
--- a/packages/client/ui-tool/README.zh.md
+++ b/packages/client/ui-tool/README.zh.md
@@ -96,7 +96,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`
这些限制定义分派深度与视图归属;它们是当前包约束。
-- **Host 不把 `run_code` 暴露为 Code Mode 程序 binding**:生产事件只产生一层分发;递归的运行时/UI 约定支持嵌套。
+- **Host 不把 `run_code` 暴露为 PTC mode 程序 binding**:生产事件只产生一层分发;递归的运行时/UI 约定支持嵌套。
- **第一方工具视图集中在本包**:它们可以通过 keyed slot 独立迁移到各自所属的业务包。
- **工具文案复用 `ui-conversation` locale namespace**:工具标题、行 chrome 与无 Cordis 的 primitive label 使用该字典;presenter model 保留 locale key 或数据,而不是已渲染文案。
diff --git a/packages/client/ui-tool/package.json b/packages/client/ui-tool/package.json
index 0e38fce093..ca96120343 100644
--- a/packages/client/ui-tool/package.json
+++ b/packages/client/ui-tool/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-tool",
"description": "Client Tool call-tree renderer and keyed per-tool presentation slot",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json
index 054dc9ddf9..242810c753 100644
--- a/packages/client/ui-trajectory/package.json
+++ b/packages/client/ui-trajectory/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-trajectory",
"description": "Trajectory event ledger with an interactive timing overview: pure-consumer plugin registering into the conversation ViewMap (no service)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-user-questions/package.json b/packages/client/ui-user-questions/package.json
index 48137b5d0a..b8059eecfb 100644
--- a/packages/client/ui-user-questions/package.json
+++ b/packages/client/ui-user-questions/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-user-questions",
"description": "Web ask_user_question composer takeover and plan-review presentation UI",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-workflow-run/README.i18n.yaml b/packages/client/ui-workflow-run/README.i18n.yaml
index 389abffb90..3df2ccbc96 100644
--- a/packages/client/ui-workflow-run/README.i18n.yaml
+++ b/packages/client/ui-workflow-run/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/ui-workflow-run/README.md
-README.md: 83918ae56de622625124ce219deb2022932df690
-README.zh.md: 8a86bff2b19b85b9d26b17c395fe546712febca2
+README.md: 1cf482bb8f9d0f8c41e82a639fa6cb656f5fd563
+README.zh.md: 2b559c666b1ea00374717b9f401ea02113459ac6
diff --git a/packages/client/ui-workflow-run/README.md b/packages/client/ui-workflow-run/README.md
index 83918ae56d..1cf482bb8f 100644
--- a/packages/client/ui-workflow-run/README.md
+++ b/packages/client/ui-workflow-run/README.md
@@ -85,7 +85,7 @@ None; this package neither assembles nor sends a provider request.
These limits define which runs produce records and what the node exposes; they are current package constraints.
-- **Only top-level calls through `dsh-tool-workflow` produce these records** — nested Code Mode calls and direct `WorkflowEngine` consumers do not.
+- **Only top-level calls through `dsh-tool-workflow` produce these records** — nested PTC mode calls and direct `WorkflowEngine` consumers do not.
- **Navigation is intentionally live-only** — terminal members remain visible for review but never expose a cold-session opener from this node.
- **The node shows run, phase, member identity, and status only** — scripts, outputs, errors, logs, usage, static topology, and controls remain outside this surface.
diff --git a/packages/client/ui-workflow-run/README.zh.md b/packages/client/ui-workflow-run/README.zh.md
index 8a86bff2b1..2b559c666b 100644
--- a/packages/client/ui-workflow-run/README.zh.md
+++ b/packages/client/ui-workflow-run/README.zh.md
@@ -85,7 +85,7 @@ kind: "package-reference"
这些限制定义哪些运行会产生记录、节点暴露什么;它们是当前包约束。
-- **只有经 `dsh-tool-workflow` 发起的顶层调用会生成这些记录**:嵌套 Code Mode 调用和直接 `WorkflowEngine` 消费方不会生成。
+- **只有经 `dsh-tool-workflow` 发起的顶层调用会生成这些记录**:嵌套 PTC mode 调用和直接 `WorkflowEngine` 消费方不会生成。
- **导航刻意只面向实时运行**:终态成员继续保留供复盘,但本节点永不为其提供冷 Session 入口。
- **节点只显示运行、阶段、成员身份与状态**:脚本、输出、错误、日志、用量、静态拓扑与控制操作都不属于本界面。
diff --git a/packages/client/ui-workflow-run/package.json b/packages/client/ui-workflow-run/package.json
index ac9548938b..bc5273310b 100644
--- a/packages/client/ui-workflow-run/package.json
+++ b/packages/client/ui-workflow-run/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-workflow-run",
"description": "Durable workflow-run Conversation Node and nested member disclosure for dsh web",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-workspace/package.json b/packages/client/ui-workspace/package.json
index b009ef95eb..6992a883c1 100644
--- a/packages/client/ui-workspace/package.json
+++ b/packages/client/ui-workspace/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-workspace",
"description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/web/package.json b/packages/client/web/package.json
index 9083a5bc21..6d663b1056 100644
--- a/packages/client/web/package.json
+++ b/packages/client/web/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-web",
"description": "Web boot kernel: static module table, Cordis loader, framework-free boot page, and UI-renderer handoff",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/code-runtime/README.i18n.yaml b/packages/code-runtime/README.i18n.yaml
index e741cad14c..b3d5e56e92 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: aea19fd7bcfa4cb018c387a0ef0fd8ad6ad499c2
-README.zh.md: 142d9bcc666d420536c774957973312c52ce33a3
+README.md: 165727f8b57d5392cca0fc8bc028f6b39efe35b2
+README.zh.md: c3c2cf04dac91d40d5a748ad58a359fa3b60e27c
diff --git a/packages/code-runtime/README.md b/packages/code-runtime/README.md
index aea19fd7bc..165727f8b5 100644
--- a/packages/code-runtime/README.md
+++ b/packages/code-runtime/README.md
@@ -35,10 +35,10 @@ These three packages together provide program execution; each README describes w
## Related documentation
-Start with the subsystem reference for the service contract, then the Code Mode design that consumes this capability and the capability-seam model it follows.
+Start with the subsystem reference for the service contract, then the PTC mode design that consumes this capability and the capability-seam model it follows.
- [Code runtime subsystem reference](../../docs/subsystems/code-runtime.md) — request/result vocabulary, bindings, and the `ctx.codeRuntime` cordis surface.
-- [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) — how the tool registry presents `run_code` to the model.
+- [PTC mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-ptc.md) — how the tool registry presents `run_code` to the model.
- [Capability seams](../../docs/capability-seams.md) — the Service Definition / Service Provider / Consumer split this family follows.
diff --git a/packages/code-runtime/README.zh.md b/packages/code-runtime/README.zh.md
index 142d9bcc66..c3c2cf04da 100644
--- a/packages/code-runtime/README.zh.md
+++ b/packages/code-runtime/README.zh.md
@@ -35,10 +35,10 @@ kind: "package-group"
## 相关文档
-先从子系统参考了解服务约定,再看消费此能力的 Code Mode 设计,以及它所遵循的能力 seam 模型。
+先从子系统参考了解服务约定,再看消费此能力的 PTC mode 设计,以及它所遵循的能力 seam 模型。
- [代码运行时子系统参考](../../docs/subsystems/code-runtime.zh.md)——请求/结果词汇、绑定与 `ctx.codeRuntime` 的 cordis 接口面。
-- [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md)——工具注册表如何把 `run_code` 呈现给模型。
+- [PTC mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md)——工具注册表如何把 `run_code` 呈现给模型。
- [能力 seam](../../docs/capability-seams.zh.md)——本家族遵循的 Service Definition / Service Provider / Consumer 拆分。
diff --git a/packages/code-runtime/code-runtime-python/README.i18n.yaml b/packages/code-runtime/code-runtime-python/README.i18n.yaml
index d1b381869f..535aee0b2d 100644
--- a/packages/code-runtime/code-runtime-python/README.i18n.yaml
+++ b/packages/code-runtime/code-runtime-python/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/code-runtime-python/README.md
-README.md: 22015809b88dd9d71d6cc8410b993a8b2ca0d93b
-README.zh.md: 124ac64d326c5d0a779ceefe6f17cb18bcca8904
+README.md: 7aea5ee4c031a36718e66a583762f61832596d04
+README.zh.md: 778cc61ef28be616b8b0ed1ce8f0c9396143a768
diff --git a/packages/code-runtime/code-runtime-python/README.md b/packages/code-runtime/code-runtime-python/README.md
index 22015809b8..7aea5ee4c0 100644
--- a/packages/code-runtime/code-runtime-python/README.md
+++ b/packages/code-runtime/code-runtime-python/README.md
@@ -94,7 +94,7 @@ Read these when the protocol contract is not enough. They move from the seam def
## Model Experience
-Indirectly, through Code Mode in `dsh-tools`, which renders the program's completion value or failure into a retained `run_code` result.
+Indirectly, through PTC mode in `dsh-tools`, which renders the program's completion value or failure into a retained `run_code` result.
#### KV Cache effect
diff --git a/packages/code-runtime/code-runtime-python/README.zh.md b/packages/code-runtime/code-runtime-python/README.zh.md
index 124ac64d32..778cc61ef2 100644
--- a/packages/code-runtime/code-runtime-python/README.zh.md
+++ b/packages/code-runtime/code-runtime-python/README.zh.md
@@ -94,7 +94,7 @@ host 侧校验会静默丢弃垃圾,因此格式错误或伪造的帧绝不会
## 模型体验
-通过 `dsh-tools` 中的 Code Mode 间接提供;后者把程序的完成值或失败渲染进一个保留的 `run_code` 结果。
+通过 `dsh-tools` 中的 PTC mode 间接提供;后者把程序的完成值或失败渲染进一个保留的 `run_code` 结果。
#### KV Cache 影响
diff --git a/packages/code-runtime/code-runtime-python/package.json b/packages/code-runtime/code-runtime-python/package.json
index 0cb5b0413d..9f49a052eb 100644
--- a/packages/code-runtime/code-runtime-python/package.json
+++ b/packages/code-runtime/code-runtime-python/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-code-runtime-python",
"description": "CPython subprocess implementation of the DeepSeek Harness code-execution seam",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/code-runtime/code-runtime-worker-thread/README.i18n.yaml b/packages/code-runtime/code-runtime-worker-thread/README.i18n.yaml
index cbe6322b17..9608351798 100644
--- a/packages/code-runtime/code-runtime-worker-thread/README.i18n.yaml
+++ b/packages/code-runtime/code-runtime-worker-thread/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/code-runtime-worker-thread/README.md
-README.md: 40f2c735f13efdb41a047c4f582ecf98220825d8
-README.zh.md: b9389d359dd8327e1e5847c7d0c78c94c2c51ccc
+README.md: 683a774b00a2305b0489a62b1b33f9d20dddffe8
+README.zh.md: 34b828081ebbde7fea70f4dd409e2980c6942eea
diff --git a/packages/code-runtime/code-runtime-worker-thread/README.md b/packages/code-runtime/code-runtime-worker-thread/README.md
index 40f2c735f1..683a774b00 100644
--- a/packages/code-runtime/code-runtime-worker-thread/README.md
+++ b/packages/code-runtime/code-runtime-worker-thread/README.md
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
-`dsh-code-runtime-worker-thread` executes TypeScript programs for the [`dsh-code-runtime`](../code-runtime/README.md) seam: each program runs in one fresh Node worker thread with host-provided bindings callable as ordinary async functions, and the run returns `{ value, logs, error? }`. It is the shipped backend for Code Mode in `dsh-tools`, so mounting it is what makes model-written TypeScript execution work in a composition. The runtime contains a program without isolating it: the trust posture is bash-equivalent, with an empty environment, a heap cap, measured busy-time and wall-clock budgets, and hard termination. Programs run once per request with no state carried between runs, and every failure — syntax error, budget expiry, abort, OOM exit, or output overflow — comes back as a result field.
+`dsh-code-runtime-worker-thread` executes TypeScript programs for the [`dsh-code-runtime`](../code-runtime/README.md) seam: each program runs in one fresh Node worker thread with host-provided bindings callable as ordinary async functions, and the run returns `{ value, logs, error? }`. It is the shipped backend for PTC mode in `dsh-tools`, so mounting it is what makes model-written TypeScript execution work in a composition. The runtime contains a program without isolating it: the trust posture is bash-equivalent, with an empty environment, a heap cap, measured busy-time and wall-clock budgets, and hard termination. Programs run once per request with no state carried between runs, and every failure — syntax error, budget expiry, abort, OOM exit, or output overflow — comes back as a result field.
## Table of Contents
@@ -25,7 +25,7 @@ English | [中文](README.zh.md)
## Use this package
-Mount this backend with the code-runtime seam when a composition should execute model-written TypeScript programs; Code Mode in `dsh-tools` then drives it through `ctx.codeRuntime` whenever the model calls `run_code`. Every execution cap is validated config, so you can size the runtime for your deployment from `cordis.yml`.
+Mount this backend with the code-runtime seam when a composition should execute model-written TypeScript programs; PTC mode in `dsh-tools` then drives it through `ctx.codeRuntime` whenever the model calls `run_code`. Every execution cap is validated config, so you can size the runtime for your deployment from `cordis.yml`.
### Minimal configuration
@@ -50,7 +50,7 @@ Every field is validated and defaulted at load; there are no other tunables. The
### What a run returns
-A successful run returns the program's lossless-JSON completion value as `result.value` and the text it printed, in order, as `result.logs`. Top-level `await` and `return` work, and the program can call the host-provided binding functions (Code Mode exposes one `tools` object) as ordinary async calls.
+A successful run returns the program's lossless-JSON completion value as `result.value` and the text it printed, in order, as `result.logs`. Top-level `await` and `return` work, and the program can call the host-provided binding functions (PTC mode exposes one `tools` object) as ordinary async calls.
### Containment, not a security boundary
@@ -72,7 +72,7 @@ This section explains the design behind the backend; observable behavior is full
### Design concept
-The backend rests on one separation: **containment, not a security boundary**. Model code has bash-equivalent trust (the [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) Trust posture), so the design optimizes for reconstructability and bounded resource use rather than for a hard multi-tenant boundary — that awaits a container-class backend. Each run gets one fresh worker, so a program's world dies with its worker: no cross-run state exists to leak or to log, and a run is reconstructable from the session log alone.
+The backend rests on one separation: **containment, not a security boundary**. Model code has bash-equivalent trust (the [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.md) Trust posture), so the design optimizes for reconstructability and bounded resource use rather than for a hard multi-tenant boundary — that awaits a container-class backend. Each run gets one fresh worker, so a program's world dies with its worker: no cross-run state exists to leak or to log, and a run is reconstructable from the session log alone.
### Execution flow
@@ -116,7 +116,7 @@ Source mode loads erasable-only `src/worker.ts` through Node's native type strip
Read these when the backend contract is not enough. They move from the seam definition to the consumer and the configuration surface.
- [Code runtime seam](../code-runtime/README.md) — the abstract contract this backend implements.
-- [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) — how `dsh-tools` consumes `ctx.codeRuntime` and presents `run_code`.
+- [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.md) — how `dsh-tools` consumes `ctx.codeRuntime` and presents `run_code`.
- [Code runtime subsystem reference](../../../docs/subsystems/code-runtime.md) — request/result vocabulary, bindings, and failure taxonomy.
- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-code-runtime-worker-thread) — every accepted config field and its source declaration.
@@ -125,7 +125,7 @@ Read these when the backend contract is not enough. They move from the seam defi
## Model Experience
-Indirectly, through Code Mode in `dsh-tools`, which renders the exact outer value when it fits or an explicit `invalid-output` / `output-limit` failure, while only the outer `run_code` result enters model context under its ordinary spill policy and binding traffic plus intermediate values remain execution-local.
+Indirectly, through PTC mode in `dsh-tools`, which renders the exact outer value when it fits or an explicit `invalid-output` / `output-limit` failure, while only the outer `run_code` result enters model context under its ordinary spill policy and binding traffic plus intermediate values remain execution-local.
#### KV Cache effect
diff --git a/packages/code-runtime/code-runtime-worker-thread/README.zh.md b/packages/code-runtime/code-runtime-worker-thread/README.zh.md
index b9389d359d..34b828081e 100644
--- a/packages/code-runtime/code-runtime-worker-thread/README.zh.md
+++ b/packages/code-runtime/code-runtime-worker-thread/README.zh.md
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
-`dsh-code-runtime-worker-thread` 为 [`dsh-code-runtime`](../code-runtime/README.zh.md) seam 执行 TypeScript 程序:每个程序都在一个全新的 Node Worker 线程中运行,宿主提供的绑定可作为普通异步函数调用,运行返回 `{ value, logs, error? }`。它是 `dsh-tools` 中 Code Mode 的已发布后端,因此挂载它正是让模型编写的 TypeScript 执行在组合中生效的方式。运行时「包含」程序,但不隔离它:信任立场与 bash 等价,并带有空环境、堆上限、实测忙碌时间与墙钟预算,以及强制终止。程序每次请求只运行一次,运行之间不保留状态;每个失败——语法错误、预算到期、中止、OOM 退出或输出溢出——都以结果字段返回。
+`dsh-code-runtime-worker-thread` 为 [`dsh-code-runtime`](../code-runtime/README.zh.md) seam 执行 TypeScript 程序:每个程序都在一个全新的 Node Worker 线程中运行,宿主提供的绑定可作为普通异步函数调用,运行返回 `{ value, logs, error? }`。它是 `dsh-tools` 中 PTC mode 的已发布后端,因此挂载它正是让模型编写的 TypeScript 执行在组合中生效的方式。运行时「包含」程序,但不隔离它:信任立场与 bash 等价,并带有空环境、堆上限、实测忙碌时间与墙钟预算,以及强制终止。程序每次请求只运行一次,运行之间不保留状态;每个失败——语法错误、预算到期、中止、OOM 退出或输出溢出——都以结果字段返回。
## 目录
@@ -25,7 +25,7 @@ kind: "package-reference"
## 使用本包
-当组合需要执行模型编写的 TypeScript 程序时,连同 code-runtime seam 一起挂载此后端;只要模型调用 `run_code`,`dsh-tools` 中的 Code Mode 就会通过 `ctx.codeRuntime` 驱动它。每个执行上限都是已验证的配置,因此你可以从 `cordis.yml` 为部署调整运行时规模。
+当组合需要执行模型编写的 TypeScript 程序时,连同 code-runtime seam 一起挂载此后端;只要模型调用 `run_code`,`dsh-tools` 中的 PTC mode 就会通过 `ctx.codeRuntime` 驱动它。每个执行上限都是已验证的配置,因此你可以从 `cordis.yml` 为部署调整运行时规模。
### 最小配置
@@ -50,7 +50,7 @@ kind: "package-reference"
### 运行返回什么
-成功的运行把程序的无损 JSON 完成值作为 `result.value` 返回,把程序打印的文本按顺序作为 `result.logs` 返回。顶层 `await`/`return` 可用,程序可以把宿主提供的绑定函数(Code Mode 暴露一个 `tools` 对象)当作普通异步调用。
+成功的运行把程序的无损 JSON 完成值作为 `result.value` 返回,把程序打印的文本按顺序作为 `result.logs` 返回。顶层 `await`/`return` 可用,程序可以把宿主提供的绑定函数(PTC mode 暴露一个 `tools` 对象)当作普通异步调用。
### 包含而非安全边界
@@ -72,7 +72,7 @@ kind: "package-reference"
### 设计理念
-后端建立在一个分离之上:**包含,而非安全边界**。模型代码拥有与 bash 等价的信任([Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md) 的 Trust posture),因此设计追求可重建性与有界资源使用,而非硬性的多租户边界——那需要等待容器级后端。每次运行使用一个全新的 worker,程序的世界随 worker 一同终止:不存在可泄漏、也无需记录的跨运行状态,仅凭会话日志即可重建一次运行。
+后端建立在一个分离之上:**包含,而非安全边界**。模型代码拥有与 bash 等价的信任([PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md) 的 Trust posture),因此设计追求可重建性与有界资源使用,而非硬性的多租户边界——那需要等待容器级后端。每次运行使用一个全新的 worker,程序的世界随 worker 一同终止:不存在可泄漏、也无需记录的跨运行状态,仅凭会话日志即可重建一次运行。
### 执行流程
@@ -116,7 +116,7 @@ kind: "package-reference"
当后端约定不够用时阅读以下内容。它们从 seam 定义进入消费方与配置面。
- [代码运行时 seam](../code-runtime/README.zh.md)——此后端实现的抽象约定。
-- [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md)——`dsh-tools` 如何消费 `ctx.codeRuntime` 并呈现 `run_code`。
+- [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md)——`dsh-tools` 如何消费 `ctx.codeRuntime` 并呈现 `run_code`。
- [代码运行时子系统参考](../../../docs/subsystems/code-runtime.zh.md)——请求/结果词汇、绑定与失败分类体系。
- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-code-runtime-worker-thread)——每个受支持配置字段及其源声明。
@@ -125,7 +125,7 @@ kind: "package-reference"
## 模型体验
-通过 `dsh-tools` 中的 Code Mode 间接提供,如果外层值能容纳则原样渲染,否则返回明确的 `invalid-output`/`output-limit` 失败,且只有外层 `run_code` 结果在其普通落盘策略下进入模型上下文,绑定通信与中间值始终只存在于执行环境中。
+通过 `dsh-tools` 中的 PTC mode 间接提供,如果外层值能容纳则原样渲染,否则返回明确的 `invalid-output`/`output-limit` 失败,且只有外层 `run_code` 结果在其普通落盘策略下进入模型上下文,绑定通信与中间值始终只存在于执行环境中。
#### KV Cache 影响
diff --git a/packages/code-runtime/code-runtime-worker-thread/package.json b/packages/code-runtime/code-runtime-worker-thread/package.json
index d655c326bc..7b4b024c71 100644
--- a/packages/code-runtime/code-runtime-worker-thread/package.json
+++ b/packages/code-runtime/code-runtime-worker-thread/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-code-runtime-worker-thread",
"description": "Worker-thread implementation of the DeepSeek Harness code-execution seam",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/code-runtime/code-runtime-worker-thread/tests/bootstrap.spec.ts b/packages/code-runtime/code-runtime-worker-thread/tests/bootstrap.spec.ts
index a2aac6d9c2..a7966d7417 100644
--- a/packages/code-runtime/code-runtime-worker-thread/tests/bootstrap.spec.ts
+++ b/packages/code-runtime/code-runtime-worker-thread/tests/bootstrap.spec.ts
@@ -62,7 +62,7 @@ async function rejectionOf(promise: Promise): Promise {
const BOOT = { maxOutputBytes: 65_536 }
const TOOL_ERROR_CLASS = { name: 'ToolCallError', memberNameProperty: 'toolName' } as const
-/** One worker declaration for the Code Mode tools namespace. */
+/** One worker declaration for the PTC mode tools namespace. */
function toolNamespace(names: string[]) {
return { global: 'tools', names, errorClass: TOOL_ERROR_CLASS }
}
diff --git a/packages/code-runtime/code-runtime/README.i18n.yaml b/packages/code-runtime/code-runtime/README.i18n.yaml
index cdf019ea55..54afe6235e 100644
--- a/packages/code-runtime/code-runtime/README.i18n.yaml
+++ b/packages/code-runtime/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/code-runtime/README.md
-README.md: 7cb07be0871bd585d40f9331a12827c92f4b87c7
-README.zh.md: 4881256012d0208707ec2ec2dbe3ba4391d587cf
+README.md: e3d43e7add4992c44fef966651f91addb81aa1eb
+README.zh.md: bcbeaa8bbdfee33baa1b29e232038e2bd6b77728
diff --git a/packages/code-runtime/code-runtime/README.md b/packages/code-runtime/code-runtime/README.md
index 7cb07be087..e3d43e7add 100644
--- a/packages/code-runtime/code-runtime/README.md
+++ b/packages/code-runtime/code-runtime/README.md
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
-`dsh-code-runtime` defines what a code runtime does: run one model-written program against a set of host-provided async functions and report `{ value, logs, error? }` — without dictating how any backend implements it. Load it in a composition with a backend and the service is available as `ctx.codeRuntime`; Code Mode in `dsh-tools` then runs model-written programs that compose tools. Every request runs once with no state carried between runs, and every program outcome — including failures — resolves as a result field rather than a rejection. The runtime knows nothing about tools or sessions: it is handed a program and named bindings, and everything tool-shaped stays with the consumer.
+`dsh-code-runtime` defines what a code runtime does: run one model-written program against a set of host-provided async functions and report `{ value, logs, error? }` — without dictating how any backend implements it. Load it in a composition with a backend and the service is available as `ctx.codeRuntime`; PTC mode in `dsh-tools` then runs model-written programs that compose tools. Every request runs once with no state carried between runs, and every program outcome — including failures — resolves as a result field rather than a rejection. The runtime knows nothing about tools or sessions: it is handed a program and named bindings, and everything tool-shaped stays with the consumer.
## Table of Contents
@@ -25,11 +25,11 @@ English | [中文](README.zh.md)
## Use this package
-Choose this package when you compose a deployment that executes model-written programs, consume `ctx.codeRuntime` directly, or build a backend that runs programs. In the shipped composition, Code Mode in `dsh-tools` is the consumer: only what the program printed and returned re-enters the conversation.
+Choose this package when you compose a deployment that executes model-written programs, consume `ctx.codeRuntime` directly, or build a backend that runs programs. In the shipped composition, PTC mode in `dsh-tools` is the consumer: only what the program printed and returned re-enters the conversation.
### Run a program
-Give the runtime a program source and one or more binding namespaces. Each namespace becomes one global object of async functions inside the program — Code Mode passes one under `tools`. The program runs as the body of an async function, so top-level `await` and `return` work; a lossless-JSON completion becomes `result.value`, emitted text arrives in order as `result.logs`, and any failure is reported in `result.error` with a kind you can branch on. The runtime never rejects for a program failure — rejection means you misused the seam, for example by submitting a run after disposal.
+Give the runtime a program source and one or more binding namespaces. Each namespace becomes one global object of async functions inside the program — PTC mode passes one under `tools`. The program runs as the body of an async function, so top-level `await` and `return` work; a lossless-JSON completion becomes `result.value`, emitted text arrives in order as `result.logs`, and any failure is reported in `result.error` with a kind you can branch on. The runtime never rejects for a program failure — rejection means you misused the seam, for example by submitting a run after disposal.
```text
const result = await ctx.codeRuntime.run({
@@ -63,7 +63,7 @@ This section explains the design behind the seam; observable behavior is fully c
### Design concept
-The package is the Service Definition role of the code-execution capability seam ([capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract `CodeRuntime extends Service` registered as `ctx.codeRuntime`, plus the vocabulary both backends and the consumer share. Providers subclass `CodeRuntime`, implement `run`, and register the service; the consumer (Code Mode in `dsh-tools`) generates the model-facing SDK and bridges tool dispatch. The runtime stays ignorant of tools and sessions by contract: it receives a program and named async bindings and returns `{ value, logs, error? }`.
+The package is the Service Definition role of the code-execution capability seam ([capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract `CodeRuntime extends Service` registered as `ctx.codeRuntime`, plus the vocabulary both backends and the consumer share. Providers subclass `CodeRuntime`, implement `run`, and register the service; the consumer (PTC mode in `dsh-tools`) generates the model-facing SDK and bridges tool dispatch. The runtime stays ignorant of tools and sessions by contract: it receives a program and named async bindings and returns `{ value, logs, error? }`.
### Service API
@@ -94,9 +94,9 @@ Binding-global and error-class names are language-portable: they must match the
## Further Exploration
-Read these when the package-level contract is not enough. They move from the Code Mode consumer to the shipped backends and the capability-seam model.
+Read these when the package-level contract is not enough. They move from the PTC mode consumer to the shipped backends and the capability-seam model.
-- [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) — how the tool registry consumes `ctx.codeRuntime` and presents `run_code` to the model.
+- [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.md) — how the tool registry consumes `ctx.codeRuntime` and presents `run_code` to the model.
- [Worker-thread backend](../code-runtime-worker-thread/README.md) — the shipped TypeScript execution backend.
- [Python protocol package](../code-runtime-python/README.md) — the wire protocol for the CPython backend.
- [Code runtime subsystem reference](../../../docs/subsystems/code-runtime.md) — request/result vocabulary, bindings, and the `ctx.codeRuntime` cordis surface.
@@ -107,7 +107,7 @@ Read these when the package-level contract is not enough. They move from the Cod
## Model Experience
-Indirectly, through Code Mode in `dsh-tools`, which exposes `run_code` and returns program logs, values, or failures as retained tool-result tokens.
+Indirectly, through PTC mode in `dsh-tools`, which exposes `run_code` and returns program logs, values, or failures as retained tool-result tokens.
#### KV Cache effect
diff --git a/packages/code-runtime/code-runtime/README.zh.md b/packages/code-runtime/code-runtime/README.zh.md
index 4881256012..bcbeaa8bbd 100644
--- a/packages/code-runtime/code-runtime/README.zh.md
+++ b/packages/code-runtime/code-runtime/README.zh.md
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
-`dsh-code-runtime` 定义代码运行时做什么:针对一组宿主提供的异步函数运行一段模型编写的程序,并报告 `{ value, logs, error? }`——不规定任何后端如何实现。在组合中与一个后端一起加载它,服务即可作为 `ctx.codeRuntime` 使用;随后 `dsh-tools` 中的 Code Mode 即可运行组合工具的模型程序。每次请求只运行一次,运行之间不保留状态;每个程序结果——包括失败——都以结果字段 resolve,而不是 reject。运行时不了解工具或会话:调用方只向它提供程序与具名绑定,所有与工具有关的内容都留在 Consumer。
+`dsh-code-runtime` 定义代码运行时做什么:针对一组宿主提供的异步函数运行一段模型编写的程序,并报告 `{ value, logs, error? }`——不规定任何后端如何实现。在组合中与一个后端一起加载它,服务即可作为 `ctx.codeRuntime` 使用;随后 `dsh-tools` 中的 PTC mode 即可运行组合工具的模型程序。每次请求只运行一次,运行之间不保留状态;每个程序结果——包括失败——都以结果字段 resolve,而不是 reject。运行时不了解工具或会话:调用方只向它提供程序与具名绑定,所有与工具有关的内容都留在 Consumer。
## 目录
@@ -25,11 +25,11 @@ kind: "package-reference"
## 使用本包
-当你要组合一个执行模型程序的部署、直接消费 `ctx.codeRuntime`,或构建运行程序的后端时,选择本包。在已发布的组合中,`dsh-tools` 里的 Code Mode 是消费方:只有程序打印和返回的内容重新进入对话。
+当你要组合一个执行模型程序的部署、直接消费 `ctx.codeRuntime`,或构建运行程序的后端时,选择本包。在已发布的组合中,`dsh-tools` 里的 PTC mode 是消费方:只有程序打印和返回的内容重新进入对话。
### 运行一个程序
-向运行时提供程序源码与一个或多个绑定命名空间。每个命名空间会成为程序内的一个全局异步函数对象——Code Mode 在 `tools` 下传入一个。程序作为异步函数的函数体运行,因此顶层 `await`/`return` 可用;无损 JSON 完成值成为 `result.value`,输出的文本按顺序进入 `result.logs`,任何失败都以 `result.error` 报告并带有可分支的 kind。运行时绝不会因程序失败而 reject——reject 意味着你误用了 seam,例如在 dispose(资源释放)后提交运行。
+向运行时提供程序源码与一个或多个绑定命名空间。每个命名空间会成为程序内的一个全局异步函数对象——PTC mode 在 `tools` 下传入一个。程序作为异步函数的函数体运行,因此顶层 `await`/`return` 可用;无损 JSON 完成值成为 `result.value`,输出的文本按顺序进入 `result.logs`,任何失败都以 `result.error` 报告并带有可分支的 kind。运行时绝不会因程序失败而 reject——reject 意味着你误用了 seam,例如在 dispose(资源释放)后提交运行。
```text
const result = await ctx.codeRuntime.run({
@@ -63,7 +63,7 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配 `[A-Za
### 设计理念
-本包是代码执行能力 seam 的 Service Definition 角色([能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)):一个注册为 `ctx.codeRuntime` 的抽象 `CodeRuntime extends Service`,加上两个后端与消费方共享的词汇。提供方继承 `CodeRuntime`、实现 `run` 并注册服务;消费方(`dsh-tools` 中的 Code Mode)生成面向模型的 SDK 并桥接工具分发。按约定,运行时不了解工具与会话:它接收程序与具名异步绑定,返回 `{ value, logs, error? }`。
+本包是代码执行能力 seam 的 Service Definition 角色([能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)):一个注册为 `ctx.codeRuntime` 的抽象 `CodeRuntime extends Service`,加上两个后端与消费方共享的词汇。提供方继承 `CodeRuntime`、实现 `run` 并注册服务;消费方(`dsh-tools` 中的 PTC mode)生成面向模型的 SDK 并桥接工具分发。按约定,运行时不了解工具与会话:它接收程序与具名异步绑定,返回 `{ value, logs, error? }`。
### 服务 API
@@ -94,9 +94,9 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配标识
## 进一步探索
-当包级约定不够用时阅读以下内容。它们从 Code Mode 消费方进入已发布的后端与能力 seam 模型。
+当包级约定不够用时阅读以下内容。它们从 PTC mode 消费方进入已发布的后端与能力 seam 模型。
-- [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md)——工具注册表如何消费 `ctx.codeRuntime` 并把 `run_code` 呈现给模型。
+- [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md)——工具注册表如何消费 `ctx.codeRuntime` 并把 `run_code` 呈现给模型。
- [Worker 线程后端](../code-runtime-worker-thread/README.zh.md)——已发布的 TypeScript 执行后端。
- [Python 协议包](../code-runtime-python/README.zh.md)——CPython 后端的协议格式。
- [代码运行时子系统参考](../../../docs/subsystems/code-runtime.zh.md)——请求/结果词汇、绑定与 `ctx.codeRuntime` 的 cordis 接口面。
@@ -107,7 +107,7 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配标识
## 模型体验
-通过 `dsh-tools` 中的 Code Mode 间接提供;后者公开 `run_code`,并将程序日志、值或失败作为保留的工具结果 token 返回。
+通过 `dsh-tools` 中的 PTC mode 间接提供;后者公开 `run_code`,并将程序日志、值或失败作为保留的工具结果 token 返回。
#### KV Cache 影响
diff --git a/packages/code-runtime/code-runtime/package.json b/packages/code-runtime/code-runtime/package.json
index b107880ba7..fda9c1a984 100644
--- a/packages/code-runtime/code-runtime/package.json
+++ b/packages/code-runtime/code-runtime/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-code-runtime",
"description": "Abstract code-execution seam (ctx.codeRuntime) for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/code-runtime/code-runtime/src/types.ts b/packages/code-runtime/code-runtime/src/types.ts
index a204b113ac..6a5adda0be 100644
--- a/packages/code-runtime/code-runtime/src/types.ts
+++ b/packages/code-runtime/code-runtime/src/types.ts
@@ -25,7 +25,7 @@ export type CodeJsonValue = null | boolean | number | string | CodeJsonValue[] |
* injects a real error constructor under `name`; rejected member calls become
* its instances and expose the exact member name through
* `memberNameProperty`. Both strings are runtime data rather than knowledge
- * of a particular consumer such as Code Mode.
+ * of a particular consumer such as PTC mode.
*/
export interface CodeBindingErrorClass {
/** Constructor global and resulting `Error.name`; same portable identifier rule as {@link CodeBindingNamespace.global}. */
diff --git a/packages/compaction/command-compact/package.json b/packages/compaction/command-compact/package.json
index 4872714c50..f5a7edf1f3 100644
--- a/packages/compaction/command-compact/package.json
+++ b/packages/compaction/command-compact/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-command-compact",
"description": "Human-facing slash command for explicit session compaction",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/compaction/compaction-basic/package.json b/packages/compaction/compaction-basic/package.json
index 8114091eeb..6f16c84c47 100644
--- a/packages/compaction/compaction-basic/package.json
+++ b/packages/compaction/compaction-basic/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-compaction-basic",
"description": "Token-meter-driven compaction policy and LLM summarization backend for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -65,6 +65,7 @@
"@deepseek-ai/dsh-token-meter": "workspace:^",
"@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts b/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts
index 1afe6be023..82eadaa71e 100644
--- a/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts
+++ b/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts
@@ -24,6 +24,7 @@ import type {
TokenUsage,
} from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import { agentEvents, type Agent, type RequestErrorAction } from '@deepseek-ai/dsh-agent'
import ToolResultPruner from '@deepseek-ai/dsh-compaction-tool-result-pruner'
@@ -73,6 +74,9 @@ class RoutedContextAdapter extends LlmAdapter {
function createContext(contextWindow = 1_000): Context {
const ctx = new Context()
void new LlmRuntime(ctx)
+ // The registry is a required injection of TokenMeter (its three
+ // projection units register in the constructor); mount it synchronously.
+ new SessionProjectionRegistry(ctx)
void new TokenMeter(ctx)
ctx.llm.registerAdapter([MODEL, 'actual', 'unlisted-provider'], new ContextAdapter(contextWindow))
return ctx
@@ -519,6 +523,7 @@ describe('pressure measurement and retention', () => {
it('re-resolves capacity after a same-model-id provider switch in one session', async () => {
const ctx = new Context()
void new LlmRuntime(ctx)
+ new SessionProjectionRegistry(ctx)
void new TokenMeter(ctx)
ctx.llm.registerAdapter(['large', 'small'], new RoutedContextAdapter({
large: 10_000,
@@ -546,6 +551,7 @@ describe('pressure measurement and retention', () => {
it('requires capacity only for proactive pressure, not provider-confirmed overflow', async () => {
const ctx = new Context()
void new LlmRuntime(ctx)
+ new SessionProjectionRegistry(ctx)
void new TokenMeter(ctx)
ctx.llm.registerAdapter(['unknown-context'], new ContextAdapter(1_000))
vi.spyOn(ctx.llm, 'resolveModelInfo').mockImplementation((provider, model) => Promise.resolve({
@@ -1163,6 +1169,7 @@ async function summarizerHarness(
): Promise<{ ctx: Context; adapter: ScriptedAdapter; compact: ExposedCompactionEngine }> {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
+ await ctx.plugin(SessionProjectionRegistry)
void new TokenMeter(ctx)
const adapter = new ScriptedAdapter(blocks, finish)
ctx.llm.registerAdapter([model], adapter)
@@ -1339,6 +1346,7 @@ describe('default one-shot summarizer', () => {
it('fails clearly when no complete summarization target can be resolved', async () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
+ await ctx.plugin(SessionProjectionRegistry)
void new TokenMeter(ctx)
const compact = new ExposedCompactionEngine(ctx, { auto: false })
await expect(compact.runSummarize(promptInput('history'), agent(Session.create(SessionId('model-less')))))
@@ -1853,6 +1861,7 @@ describe('automatic listener and loader composition', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
const meterFiber = await ctx.plugin(TokenMeter)
const compactFiber = await ctx.plugin(BasicCompactionEngine, { auto: false })
@@ -1866,6 +1875,7 @@ describe('automatic listener and loader composition', () => {
it('removes its automatic listener with the plugin fiber', async () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(TokenMeter)
const fiber = await ctx.plugin(TestCompactionEngine, {
thresholdRatio: 0.5,
@@ -1898,6 +1908,7 @@ describe('route-priced image pressure', () => {
function pricedContext(contextWindow = 1_000): Context {
const ctx = new Context()
void new LlmRuntime(ctx)
+ new SessionProjectionRegistry(ctx)
void new TokenMeter(ctx)
ctx.llm.registerAdapter([MODEL], new PricedContextAdapter(contextWindow))
return ctx
diff --git a/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts b/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts
index 46d0384803..1dacfe2114 100644
--- a/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts
+++ b/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts
@@ -13,6 +13,7 @@ import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import * as LlmRetry from '@deepseek-ai/dsh-llm-retry'
import { Session, SessionId, type SessionEvent, type SurfaceEvent } from '@deepseek-ai/dsh-session'
@@ -150,6 +151,9 @@ async function harness(toolSteps: number): Promise<{ ctx: Context; compact: Repr
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
+ // AgentLoop and TokenMeter both declare the registry as a required
+ // injection; mount it before either activates.
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(TokenMeter)
ctx.llm.registerAdapter(['mock'], new StepwiseToolAdapter(toolSteps))
@@ -312,6 +316,7 @@ describe('context-overflow recovery across the real loop and compaction-basic',
const adapter = new OverflowRecoveryAdapter(delivery)
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(TokenMeter)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -390,6 +395,7 @@ describe('context-overflow recovery across the real loop and compaction-basic',
const adapter = new OverflowRecoveryAdapter('thrown', true)
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(LlmRetry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(TokenMeter)
diff --git a/packages/compaction/compaction-basic/tests/loader-composition.spec.ts b/packages/compaction/compaction-basic/tests/loader-composition.spec.ts
index c4efe48f97..1fc9d86bf9 100644
--- a/packages/compaction/compaction-basic/tests/loader-composition.spec.ts
+++ b/packages/compaction/compaction-basic/tests/loader-composition.spec.ts
@@ -8,6 +8,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic'
import ToolResultPruner from '@deepseek-ai/dsh-compaction-tool-result-pruner'
@@ -34,6 +35,7 @@ async function loadYaml(lines: readonly string[]): Promise {
const modules = new Map([
['@deepseek-ai/dsh-llm', LlmRuntime],
['@deepseek-ai/dsh-session', SessionStore],
+ ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
['@deepseek-ai/dsh-token-meter', TokenMeter],
['@deepseek-ai/dsh-compaction-tool-result-pruner', ToolResultPruner],
['@deepseek-ai/dsh-compaction-basic', BasicCompactionEngine],
@@ -58,6 +60,7 @@ describe('real Loader composition', () => {
const loaded = await loadYaml([
"- name: '@deepseek-ai/dsh-llm'",
"- name: '@deepseek-ai/dsh-session'",
+ "- name: '@deepseek-ai/dsh-session-projection'",
"- name: '@deepseek-ai/dsh-token-meter'",
"- name: '@deepseek-ai/dsh-compaction-tool-result-pruner'",
' config:',
@@ -86,6 +89,7 @@ describe('real Loader composition', () => {
it('rejects stale token-meter config after Schemastery normalization', async () => {
context = new Context()
+ await context.plugin(SessionProjectionRegistry)
await expect(context.plugin(TokenMeter, {
contextWindow: 4096,
} as never)).rejects.toThrow(/TokenMeterConfig: unknown key "contextWindow"/)
@@ -95,6 +99,7 @@ describe('real Loader composition', () => {
context = new Context()
await context.plugin(LlmRuntime)
await context.plugin(SessionStore)
+ await context.plugin(SessionProjectionRegistry)
await context.plugin(TokenMeter)
await expect(context.plugin(BasicCompactionEngine, {
models: { legacy: { thresholdRatio: 0.5 } },
@@ -105,6 +110,7 @@ describe('real Loader composition', () => {
context = new Context()
await context.plugin(LlmRuntime)
await context.plugin(SessionStore)
+ await context.plugin(SessionProjectionRegistry)
await context.plugin(TokenMeter)
await expect(context.plugin(BasicCompactionEngine, {
retainRatio: 0.2,
@@ -120,6 +126,7 @@ describe('real Loader composition', () => {
context = new Context()
await context.plugin(LlmRuntime)
await context.plugin(SessionStore)
+ await context.plugin(SessionProjectionRegistry)
await context.plugin(TokenMeter)
await expect(context.plugin(BasicCompactionEngine, {
summarizationProvider: 'default-provider',
diff --git a/packages/compaction/compaction-basic/tests/manual-compaction.spec.ts b/packages/compaction/compaction-basic/tests/manual-compaction.spec.ts
index 9e400f7741..a0497f8041 100644
--- a/packages/compaction/compaction-basic/tests/manual-compaction.spec.ts
+++ b/packages/compaction/compaction-basic/tests/manual-compaction.spec.ts
@@ -25,6 +25,7 @@ import type {
TokenUsage,
} from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -105,6 +106,7 @@ async function loopHarness(): Promise {
await ctx.plugin(AgentLoopInvariant)
await ctx.plugin(CompactionInvariant)
await ctx.plugin(CompactionBasicInvariant)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(TokenMeter)
const adapter = new TextAdapter()
@@ -221,6 +223,7 @@ function detachedService(): { ctx: Context; compact: GatedCompactionEngine; flus
const ctx = new Context()
void new LlmRuntime(ctx)
void new SessionStore(ctx)
+ new SessionProjectionRegistry(ctx)
void new TokenMeter(ctx)
ctx.llm.registerAdapter([MODEL], new TextAdapter())
let flushes = 0
diff --git a/packages/compaction/compaction-tool-result-pruner/package.json b/packages/compaction/compaction-tool-result-pruner/package.json
index 7bed09943a..64cd5f58cb 100644
--- a/packages/compaction/compaction-tool-result-pruner/package.json
+++ b/packages/compaction/compaction-tool-result-pruner/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-compaction-tool-result-pruner",
"description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -50,6 +50,7 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-token-meter": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/compaction/compaction-tool-result-pruner/tests/loader-composition.spec.ts b/packages/compaction/compaction-tool-result-pruner/tests/loader-composition.spec.ts
index 7162524078..d70283b98a 100644
--- a/packages/compaction/compaction-tool-result-pruner/tests/loader-composition.spec.ts
+++ b/packages/compaction/compaction-tool-result-pruner/tests/loader-composition.spec.ts
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import ToolResultPruner from '@deepseek-ai/dsh-compaction-tool-result-pruner'
@@ -24,6 +25,7 @@ describe('compaction-tool-result-pruner real Loader composition', () => {
root = await mkdtemp(join(tmpdir(), 'dsh-compact-tool-result-prune-loader-'))
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
+ "- name: '@deepseek-ai/dsh-session-projection'",
"- name: '@deepseek-ai/dsh-token-meter'",
"- name: '@deepseek-ai/dsh-compaction-tool-result-pruner'",
' config:',
@@ -40,6 +42,7 @@ describe('compaction-tool-result-pruner real Loader composition', () => {
context.loader.internal = {
version: 'v2',
async import(specifier: string) {
+ if (specifier === '@deepseek-ai/dsh-session-projection') return SessionProjectionRegistry
if (specifier === '@deepseek-ai/dsh-token-meter') return TokenMeter
if (specifier === '@deepseek-ai/dsh-compaction-tool-result-pruner') return ToolResultPruner
throw new Error(`unexpected Loader import: ${specifier}`)
@@ -61,8 +64,9 @@ describe('compaction-tool-result-pruner real Loader composition', () => {
it('rejects stale config after plugin schema normalization', async () => {
context = new Context()
- // Satisfy the declared injection first: config normalization runs in the
+ // Satisfy the declared injections first: config normalization runs in the
// service constructor, which a pending fiber never reaches.
+ await context.plugin(SessionProjectionRegistry)
await context.plugin(TokenMeter)
await expect(context.plugin(ToolResultPruner, {
maxChars: 100,
diff --git a/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts b/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts
index c634e11bdb..e21a3635a7 100644
--- a/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts
+++ b/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts
@@ -9,6 +9,7 @@ import SessionStore, {
import type { SurfaceEvent } from '@deepseek-ai/dsh-session'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import ToolResultPruner, {
codePointLength,
@@ -29,12 +30,15 @@ function service(config: ToolResultPruneConfig = SMALL): ToolResultPruner {
const ctx = new Context()
// Service constructors self-register, so `ctx.tokenMeter` resolves for the
// shadow-price pricing without a full plugin boot.
+ new SessionProjectionRegistry(ctx)
void new TokenMeter(ctx)
return new ToolResultPruner(ctx, config)
}
/** Pricing oracle mirroring the service's estimator for expectations. */
-const METER = new TokenMeter(new Context())
+const METER_CTX = new Context()
+new SessionProjectionRegistry(METER_CTX)
+const METER = new TokenMeter(METER_CTX)
function appendToolStep(
session: Session,
@@ -258,6 +262,7 @@ describe('ToolResultPruner session transaction', () => {
it('runs under real invariants between closed steps but not outside a turn', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(TokenMeter)
diff --git a/packages/compaction/compaction/package.json b/packages/compaction/compaction/package.json
index e995cd3d16..9684d9f3bc 100644
--- a/packages/compaction/compaction/package.json
+++ b/packages/compaction/compaction/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-compaction",
"description": "Abstract compaction service seam (ctx.compaction) for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/context/agent-instructions/README.i18n.yaml b/packages/context/agent-instructions/README.i18n.yaml
index b83ce5e8e4..19102ec246 100644
--- a/packages/context/agent-instructions/README.i18n.yaml
+++ b/packages/context/agent-instructions/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/agent-instructions/README.md
-README.md: 903f054bd53a423e0d57970d72d3144795d2e884
-README.zh.md: 447b4843dd4c92b938261f5d85b702c51eb9bff7
+README.md: 798134a9e11a4e2cf6337f1f7a2d9c79410860eb
+README.zh.md: d187494ec0f6a907df1907d6420f589792beff8b
diff --git a/packages/context/agent-instructions/README.md b/packages/context/agent-instructions/README.md
index 903f054bd5..798134a9e1 100644
--- a/packages/context/agent-instructions/README.md
+++ b/packages/context/agent-instructions/README.md
@@ -172,7 +172,7 @@ These instructions apply to work under `packages/app`. Use them as guidance when
#### Token effect
-Each discovered scope adds bounded history tokens until compaction. Unchanged content is suppressed by visible session state plus version/digest comparison, and Code Mode defers the same message until after the outer `run_code` result and its enclosing durable step.
+Each discovered scope adds bounded history tokens until compaction. Unchanged content is suppressed by visible session state plus version/digest comparison, and PTC mode defers the same message until after the outer `run_code` result and its enclosing durable step.
#### KV Cache effect
diff --git a/packages/context/agent-instructions/README.zh.md b/packages/context/agent-instructions/README.zh.md
index 447b4843dd..d187494ec0 100644
--- a/packages/context/agent-instructions/README.zh.md
+++ b/packages/context/agent-instructions/README.zh.md
@@ -172,7 +172,7 @@ These instructions apply to work under `packages/app`. Use them as guidance when
#### Token 影响
-每个已发现 scope 都会添加有界历史 token,直到压缩。可见会话状态与版本/digest 比较会抑制未更改内容,Code Mode 将同一消息延迟至外层 `run_code` 结果及其所属持久步骤之后。
+每个已发现 scope 都会添加有界历史 token,直到压缩。可见会话状态与版本/digest 比较会抑制未更改内容,PTC mode 将同一消息延迟至外层 `run_code` 结果及其所属持久步骤之后。
#### KV Cache 影响
diff --git a/packages/context/agent-instructions/package.json b/packages/context/agent-instructions/package.json
index 419b8d5c5e..7253ece9df 100644
--- a/packages/context/agent-instructions/package.json
+++ b/packages/context/agent-instructions/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-instructions",
"description": "Workspace context loader for AGENTS.md/CLAUDE.md instruction files",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -38,6 +38,7 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
@@ -58,6 +59,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tool-fs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/context/agent-instructions/src/index.ts b/packages/context/agent-instructions/src/index.ts
index ab68bce9d0..ace82c24b2 100644
--- a/packages/context/agent-instructions/src/index.ts
+++ b/packages/context/agent-instructions/src/index.ts
@@ -14,6 +14,7 @@ import { isDeepStrictEqual } from 'node:util'
import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
+import type {} from '@deepseek-ai/dsh-session-projection'
import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import { Config, resolveConfig, workspaceBaselineIdentity, type ResolvedConfig } from './config.ts'
import { findProjectRoot, loadBaselineInstructionSet } from './files.ts'
@@ -29,6 +30,8 @@ import {
import type { AgentInstructionChange } from './render.ts'
export { Config, name }
+/** Services required by workspace instruction projection. */
+export const inject = ['sessionProjections']
export {
discoverBaselineInstructionFiles,
loadBaselineInstructions,
@@ -99,7 +102,6 @@ export function apply(ctx: Context, config: Config): void {
const projectionTails = new WeakMap>()
// Execution ancestry and the enclosing durable step are the two commit
// boundaries before an asynchronous projection may mutate the agent inbox.
- const openSteps = new WeakMap()
const stepTouches = new WeakMap()
const compose = async (
@@ -280,15 +282,13 @@ export function apply(ctx: Context, config: Config): void {
}
const stepIsOpen = (session: Session): boolean => {
- const known = openSteps.get(session)
- if (known !== undefined) return known
- let open = false
- for (const event of session.events) {
- if (event.type === 'step/start') open = true
- else if (event.type === 'step/end' || event.type === 'turn/end') open = false
+ const boundary = ctx.sessionProjections.stateOf(session, 'turnBoundary')
+ if (boundary === undefined) {
+ throw new Error('agent-instructions requires the turnBoundary session projection')
}
- openSteps.set(session, open)
- return open
+ return boundary.openTurnStartSeq !== null
+ && boundary.lastStepBoundary?.kind === 'start'
+ && boundary.lastStepBoundary.seq > boundary.openTurnStartSeq
}
const projectTouch = (touch: ProjectionTouch): void => {
@@ -303,16 +303,7 @@ export function apply(ctx: Context, config: Config): void {
}
ctx.on('session/event', (session, event) => {
- if (event.type === 'step/start') {
- openSteps.set(session, true)
- return
- }
- if (event.type === 'turn/end') {
- openSteps.set(session, false)
- return
- }
if (event.type !== 'step/end') return
- openSteps.set(session, false)
const pending = stepTouches.get(session)
if (pending === undefined) return
stepTouches.delete(session)
diff --git a/packages/context/agent-instructions/tests/agent-instructions.e2e.ts b/packages/context/agent-instructions/tests/agent-instructions.e2e.ts
index 392936b4c2..7f468bb6c8 100644
--- a/packages/context/agent-instructions/tests/agent-instructions.e2e.ts
+++ b/packages/context/agent-instructions/tests/agent-instructions.e2e.ts
@@ -11,6 +11,7 @@ import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import * as WorkspaceContext from '@deepseek-ai/dsh-agent-instructions'
import { candidateScopeKey } from '../src/render.ts'
@@ -39,6 +40,7 @@ async function harness(): Promise<{ ctx: Context; agent: Agent }> {
ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona: 'Answer the user exactly and concisely.' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/context/agent-instructions/tests/agent-instructions.spec.ts b/packages/context/agent-instructions/tests/agent-instructions.spec.ts
index a1101a390c..5c9c3e7f2f 100644
--- a/packages/context/agent-instructions/tests/agent-instructions.spec.ts
+++ b/packages/context/agent-instructions/tests/agent-instructions.spec.ts
@@ -7,8 +7,9 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import * as workspaceContext from '@deepseek-ai/dsh-agent-instructions'
import LlmRuntime, { createUserMessage, ToolCallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent, type UserMessage } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import AgentRegistry, { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
-import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import AgentLoop, { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import type {
FsDirEntry,
@@ -167,9 +168,15 @@ class BlockingReadFileSystem extends RecordingFileSystem {
}
}
+async function mountWorkspaceContextPlugin(ctx: Context, config: workspaceContext.Config): Promise>> {
+ if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry)
+ ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
+ return ctx.plugin(workspaceContext, config)
+}
+
async function mountWorkspaceContext(ctx: Context, config: workspaceContext.Config): Promise>> {
await ctx.plugin(LocalFileSystem, { cwd: '/' })
- return ctx.plugin(workspaceContext, config)
+ return mountWorkspaceContextPlugin(ctx, config)
}
async function mountFileToolsAndWorkspaceContext(ctx: Context, config: workspaceContext.Config): Promise>> {
@@ -177,7 +184,7 @@ async function mountFileToolsAndWorkspaceContext(ctx: Context, config: workspace
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalFileSystem, { cwd: '/' })
await ctx.plugin(ToolFs)
- return ctx.plugin(workspaceContext, config)
+ return mountWorkspaceContextPlugin(ctx, config)
}
function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
@@ -985,26 +992,47 @@ describe('workspace context request injection', () => {
it('requires an explicit maxBytes configuration', async () => {
const ctx = new Context()
- await expect(ctx.plugin(workspaceContext, {} as workspaceContext.Config)).rejects.toThrow(/maxBytes/)
+ await expect(mountWorkspaceContextPlugin(ctx, {} as workspaceContext.Config)).rejects.toThrow(/maxBytes/)
})
it('mounts without requiring a filesystem provider', async () => {
const ctx = new Context()
try {
- await ctx.plugin(workspaceContext, { maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { maxBytes: 65536 })
} finally {
await ctx.fiber.dispose()
}
})
- it('does not declare fs as a static inject dependency', () => {
- expect('inject' in workspaceContext).toBe(false)
+ it('requires projections without making the optional filesystem a static dependency', () => {
+ expect(workspaceContext.inject).toEqual(['sessionProjections'])
+ })
+
+ it('rejects a file-touch projection when the turn boundary unit is absent', async () => {
+ const ctx = new Context()
+ await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(workspaceContext, { maxBytes: 65536 })
+ const exec = stubToolExecution({
+ callId: ToolCallId('missing-turn-boundary'),
+ name: 'read',
+ arguments: { file_path: 'file.txt' },
+ agent: stubAgent('/virtual/repo'),
+ signal: testToolSignal,
+ })
+
+ expect(() => {
+ ctx.emit('tools/result', exec, {
+ content: [{ type: 'text', text: 'ok' }],
+ isError: false,
+ value: null,
+ })
+ }).toThrow('agent-instructions requires the turnBoundary session projection')
})
it('does not inject baseline context when no filesystem provider is present', async () => {
const ctx = new Context()
try {
- await ctx.plugin(workspaceContext, { maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { maxBytes: 65536 })
const agent = stubAgent('/virtual/repo')
await composeBaselinePrefix(ctx, agent)
@@ -1111,7 +1139,7 @@ describe('workspace context request injection', () => {
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'repo rule' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const original = stubAgent(root)
await composeBaselinePrefix(ctx, original)
@@ -1355,7 +1383,7 @@ describe('workspace context request injection', () => {
expect(inserted?.source).toMatchObject({ kind: 'agent-instructions', baseline: true })
await fiber.dispose()
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const resumed = stubAgent(root, [...original.session.events])
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
const claimed = resumed.inbox.claim('next-step', 1)
@@ -1401,7 +1429,7 @@ describe('workspace context request injection', () => {
await write(join(root, 'AGENTS.md'), 'new repo rule')
await fiber.dispose()
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const resumed = stubAgent(root, [...original.session.events])
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
const staleClaim = resumed.inbox.claim('next-step', 1)
@@ -1454,7 +1482,7 @@ describe('workspace context request injection', () => {
await originalCtx.fiber.dispose()
if (provideFs) await resumedCtx.plugin(LocalFileSystem, { cwd: '/' })
- await resumedCtx.plugin(workspaceContext, { dshHome: home, maxBytes })
+ await mountWorkspaceContextPlugin(resumedCtx, { dshHome: home, maxBytes })
const resumed = stubAgent(root, [...original.session.events])
agentEvents(resumedCtx, resumed).emit('agent/session-start', { source: 'resume' })
const claimed = resumed.inbox.claim('next-step', 1)
@@ -1649,7 +1677,7 @@ describe('workspace context request injection', () => {
// Hot remount over the live session: the durable baseline remains
// visible, so the fresh mount does not append a duplicate.
await fiber.dispose()
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
await composeBaselinePrefix(ctx, agent)
expect(baselineEvents(agent)).toHaveLength(1)
@@ -1679,7 +1707,7 @@ describe('workspace context request injection', () => {
await write(join(root, 'AGENTS.md'), 'repo rule')
const ctx = new Context()
await ctx.plugin(LocalFileSystem, { cwd: '/' })
- const fiber = await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ const fiber = await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
const baseline = baselineEvents(agent)[0]
@@ -1694,7 +1722,7 @@ describe('workspace context request injection', () => {
})
await fiber.dispose()
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
await composeBaselinePrefix(ctx, agent)
expect(baselineEvents(agent)).toHaveLength(2)
@@ -1981,7 +2009,7 @@ describe('workspace context request injection', () => {
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'ctx.fs rule' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
@@ -2004,7 +2032,7 @@ describe('workspace context request injection', () => {
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'provider-only rule' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
@@ -2046,7 +2074,7 @@ describe('workspace context request injection', () => {
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'far too large' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536, maxSourceBytes: 4 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536, maxSourceBytes: 4 })
const prefix = await composeBaselinePrefix(ctx, stubAgent(root))
@@ -2071,7 +2099,7 @@ describe('workspace context request injection', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(instructionPath, { type: 'file', content: 'far too large' })
fs.omitSizes.add(instructionPath)
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536, maxSourceBytes: 4 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536, maxSourceBytes: 4 })
const prefix = await composeBaselinePrefix(ctx, stubAgent(root))
@@ -2094,7 +2122,7 @@ describe('workspace context request injection', () => {
const fs = ctx.fs as BlockingReadFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'blocked' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const controller = new AbortController()
const reason = new Error('cancel prefix')
const pending = agentEvents(ctx, stubAgent(root)).waterfall(
@@ -2128,7 +2156,7 @@ describe('workspace context request injection', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(home, 'AGENTS.md'), { type: 'file', content: 'ctx global rule' })
fs.entries.set(join(root, 'CLAUDE.md'), { type: 'file', content: 'ctx claude rule' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
@@ -2154,7 +2182,7 @@ describe('workspace context request injection', () => {
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'directory' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
@@ -2177,7 +2205,7 @@ describe('workspace context request injection', () => {
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
@@ -2200,7 +2228,7 @@ describe('workspace context request injection', () => {
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.throwOnStat.add(join(root, 'AGENTS.md'))
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
@@ -2222,7 +2250,7 @@ describe('workspace context request injection', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.throwOnStat.add(join(root, 'AGENTS.md'))
fs.entries.set(join(root, 'CLAUDE.md'), { type: 'file', content: 'claude sibling rule' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
@@ -2247,7 +2275,7 @@ describe('workspace context request injection', () => {
const fs = ctx.fs as RecordingFileSystem
fs.throwOnStat.add(join(root, '.git'))
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'repo rule' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
@@ -2297,7 +2325,7 @@ describe('workspace context request injection', () => {
await write(join(cwd, 'AGENTS.md'), 'child schema default rule')
const ctx = new Context()
await ctx.plugin(LocalFileSystem, { cwd: '/' })
- await ctx.plugin(workspaceContext, { maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { maxBytes: 65536 })
const agent = stubAgent(cwd)
await composeBaselinePrefix(ctx, agent)
@@ -2318,7 +2346,7 @@ describe('workspace context request injection', () => {
await write(join(root, 'AGENTS.local.md'), 'local rule')
const ctx = new Context()
await ctx.plugin(LocalFileSystem, { cwd: '/' })
- await ctx.plugin(workspaceContext, { maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { maxBytes: 65536 })
const agent = stubAgent(root)
await composeBaselinePrefix(ctx, agent)
@@ -2514,7 +2542,7 @@ describe('dynamic nested workspace context injection', () => {
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalFileSystem, { cwd: '/' })
await ctx.plugin(ToolFs)
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
const agent = ctx.agentLoop.create(SessionId('workspace-context-abort'), { provider: 'mock', model: 'mock' }, { cwd: root })
@@ -2590,7 +2618,7 @@ describe('dynamic nested workspace context injection', () => {
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'file', content: 'nested' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const controller = new AbortController()
const reason = new Error('cancel dynamic reconciliation')
controller.abort(reason)
@@ -2853,7 +2881,7 @@ describe('dynamic nested workspace context injection', () => {
fs.omitSizes.add(instructionPath)
fs.entries.set(join(root, 'pkg/file.txt'), { type: 'file', content: 'hello' })
await ctx.plugin(ToolFs)
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const first = await ctx.tools.execute({
@@ -2890,7 +2918,7 @@ describe('dynamic nested workspace context injection', () => {
fs.entries.set(instructionPath, { type: 'file', content: 'same package rule', version: FsVersion('revision-1') })
fs.entries.set(join(root, 'pkg/file.txt'), { type: 'file', content: 'hello' })
await ctx.plugin(ToolFs)
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
await ctx.tools.execute({
@@ -2935,7 +2963,7 @@ describe('dynamic nested workspace context injection', () => {
fs.entries.set(instructionPath, { type: 'file', content: 'shared path, separate sessions' })
fs.entries.set(join(root, 'pkg/file.txt'), { type: 'file', content: 'hello' })
await ctx.plugin(ToolFs)
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const firstAgent = stubAgent(root)
const secondAgent = stubAgent(root)
@@ -3471,7 +3499,7 @@ describe('dynamic nested workspace context injection', () => {
fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'file', content: 'provider package rule' })
fs.entries.set(join(root, 'pkg/file.txt'), { type: 'file', content: 'hello' })
await ctx.plugin(ToolFs)
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const first = await ctx.tools.execute({
@@ -3850,7 +3878,7 @@ describe('dynamic nested workspace context injection', () => {
fs.entries.set(join(root, 'pkg/deep/file.txt'), { type: 'file', content: 'hello' })
fs.throwOnRead.add(nested)
await ctx.plugin(ToolFs)
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const result = await ctx.tools.execute({
@@ -3991,7 +4019,7 @@ describe('dynamic nested workspace context injection', () => {
? { kind: 'block' as const, feedback: [{ type: 'text' as const, text: 'outer policy block' }] }
: downstream
})
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const blocked = await ctx.tools.execute({
@@ -4060,7 +4088,7 @@ describe('dynamic nested workspace context injection', () => {
? { kind: 'block' as const, feedback: [{ type: 'text' as const, text: 'outer composite block' }] }
: downstream
})
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const blocked = await ctx.tools.execute({
@@ -4084,7 +4112,7 @@ describe('dynamic nested workspace context injection', () => {
const ctx = new Context()
try {
await ctx.plugin(RecordingFileSystem)
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'file', content: 'nested package rule' })
@@ -4159,7 +4187,7 @@ describe('dynamic nested workspace context injection', () => {
agent.session.append('step/start', { turn: 1, step: 1 })
agent.session.append('step/end', { turn: 1, step: 1 })
agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
ctx.emit('tools/result', stubToolExecution({
signal: testToolSignal,
@@ -4182,7 +4210,7 @@ describe('dynamic nested workspace context injection', () => {
const ctx = new Context()
try {
await ctx.plugin(RecordingFileSystem)
- await ctx.plugin(workspaceContext, { maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { maxBytes: 65536 })
const fs = ctx.fs as RecordingFileSystem
const agent = stubAgent('/')
const plainResult = { callId: ToolCallId('plain'), content: [], isError: false as const, value: null }
@@ -4231,7 +4259,7 @@ describe('dynamic nested workspace context injection', () => {
const ctx = new Context()
try {
await ctx.plugin(RecordingFileSystem)
- await ctx.plugin(workspaceContext, { maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { maxBytes: 65536 })
const fs = ctx.fs as RecordingFileSystem
const root = resolve('/')
const agent = stubAgent(root)
@@ -4297,7 +4325,7 @@ describe('dynamic nested workspace context injection', () => {
fs.entries.set(instructionPath, { type: 'file', content: 'x'.repeat(1000) })
fs.entries.set(join(root, 'pkg/file.txt'), { type: 'file', content: 'hello' })
await ctx.plugin(ToolFs)
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 20 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 20 })
const agent = stubAgent(root)
const first = await ctx.tools.execute({
@@ -4419,7 +4447,7 @@ describe('workspace context inbox synchronization', () => {
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'file', content: 'tiny-budget rule' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 1 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 1 })
const agent = stubAgent(root)
ctx.emit('tools/result', stubToolExecution({
signal: testToolSignal,
@@ -4495,7 +4523,7 @@ describe('workspace context inbox synchronization', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'a/AGENTS.md'), { type: 'file', content: 'restored A' })
fs.entries.set(join(root, 'b/AGENTS.md'), { type: 'file', content: 'restored B' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const first = stubToolExecution({
signal: testToolSignal,
@@ -4535,7 +4563,7 @@ describe('workspace context inbox synchronization', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'a/AGENTS.md'), { type: 'file', content: 'scope A' })
fs.entries.set(join(root, 'b/AGENTS.md'), { type: 'file', content: 'scope B' })
- await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
+ await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const first = stubToolExecution({
signal: testToolSignal,
diff --git a/packages/context/agent-instructions/tsconfig.json b/packages/context/agent-instructions/tsconfig.json
index ecdccf49b5..b22904487a 100644
--- a/packages/context/agent-instructions/tsconfig.json
+++ b/packages/context/agent-instructions/tsconfig.json
@@ -23,6 +23,9 @@
{
"path": "../../core/session"
},
+ {
+ "path": "../../session/session-projection"
+ },
{
"path": "../../core/tools"
},
diff --git a/packages/context/file-reference-local/package.json b/packages/context/file-reference-local/package.json
index 4c2170ac60..c86b7350c6 100644
--- a/packages/context/file-reference-local/package.json
+++ b/packages/context/file-reference-local/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-file-reference-local",
"description": "Local-filesystem ctx.fileReferences provider with bounded fuzzy indexes",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/context/file-reference/package.json b/packages/context/file-reference/package.json
index 1713c8d341..a84e7449ca 100644
--- a/packages/context/file-reference/package.json
+++ b/packages/context/file-reference/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-file-reference",
"description": "File-reference discovery contract and shared @file grammar",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/context/session-reference/package.json b/packages/context/session-reference/package.json
index e28859eea6..72c5bb1891 100644
--- a/packages/context/session-reference/package.json
+++ b/packages/context/session-reference/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-session-reference",
"description": "Cross-session snapshot references and durable untrusted model context (ctx.sessionReferenceResolver)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json
index 4ed058608b..9a553c4451 100644
--- a/packages/context/time-context/package.json
+++ b/packages/context/time-context/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-time-context",
"description": "Opt-in durable per-step context with the current time and elapsed time",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -32,14 +32,16 @@
],
"license": "MIT",
"dependencies": {
- "@deepseek-ai/schemastery": "workspace:^"
+ "@deepseek-ai/schemastery": "workspace:^",
+ "zod": "^4.4.3"
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -57,6 +59,7 @@
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts
index b8320c2d50..8a1e7880c6 100644
--- a/packages/context/time-context/src/index.ts
+++ b/packages/context/time-context/src/index.ts
@@ -7,9 +7,11 @@
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
+import { z as zod } from 'zod'
import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-llm'
+import type {} from '@deepseek-ai/dsh-session-projection'
import {
deriveBrowserTimeZoneContext,
renderBrowserTimeZoneContext,
@@ -20,8 +22,27 @@ import { createTimestampFormatter, formatTimestamp } from './timestamp.ts'
/** Cordis plugin name used by loader diagnostics. */
export const name = 'time-context'
+declare module '@deepseek-ai/dsh-session-projection/types' {
+ interface SessionProjectionStateMap {
+ /** Latest time-context readings. */
+ timeContext: TimeContextProjection
+ }
+}
+
+const timeContextStateSchema = zod.object({
+ /** Time of the latest model-visible event (user/assistant message, tool result), or null. */
+ lastMessageTime: zod.number().nullable(),
+ /** Time of this plugin's latest durable injection, or null. */
+ lastInjectionTime: zod.number().nullable(),
+ /** Latest injection time in the open turn, or null before that turn receives one. */
+ lastTurnInjectionTime: zod.number().nullable(),
+})
+
+/** Folded time-context readings. */
+type TimeContextProjection = zod.infer
+
/** The agent registry that owns pre-step processing. */
-export const inject = ['agents']
+export const inject = ['agents', 'sessionProjections']
/** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */
export interface Config {
@@ -54,47 +75,6 @@ function formatDuration(elapsedMs: number): string {
return parts.join(' ')
}
-/** Find the latest model-visible event, excluding this plugin's pending append. */
-function precedingMessageTime(agent: Agent): number | undefined {
- for (const event of [...agent.session.events].reverse()) {
- switch (event.type) {
- case 'user/message':
- case 'assistant/message':
- case 'tool/result':
- return event.time
- default:
- // Merge-extensible session events: non-surface records are not messages.
- break
- }
- }
- return undefined
-}
-
-/** Find the preceding time-context event within the open turn. */
-function precedingStepContextTime(agent: Agent, turn: number): number | undefined {
- for (const event of [...agent.session.events].reverse()) {
- if (event.type === 'turn/start' && event.data.turn === turn) return undefined
- if (event.type === 'user/message'
- && event.data.source.kind === 'plugin'
- && event.data.source.plugin === name) {
- return event.time
- }
- }
- return undefined
-}
-
-/** Find this plugin's latest durable injection, including a shadowed surface event. */
-function latestInjectionTime(agent: Agent): number | undefined {
- for (const event of [...agent.session.events].reverse()) {
- if (event.type === 'user/message'
- && event.data.source.kind === 'plugin'
- && event.data.source.plugin === name) {
- return event.time
- }
- }
- return undefined
-}
-
/** Collect already-entered and proposed user messages belonging to one open turn. */
function requestMessages(agent: Agent, turn: number, proposed: readonly UserMessage[]): UserMessage[] {
const start = agent.session.events.findLastIndex(
@@ -167,6 +147,34 @@ export function apply(ctx: Context, config: Config): void {
return created
}
+ ctx.sessionProjections.register({
+ key: 'timeContext',
+ stateVersion: 2,
+ stateSchema: timeContextStateSchema,
+ init: () => ({ lastMessageTime: null, lastInjectionTime: null, lastTurnInjectionTime: null }),
+ apply: (state, event) => {
+ if (event.type === 'turn/start' || event.type === 'turn/end') {
+ return state.lastTurnInjectionTime === null ? state : { ...state, lastTurnInjectionTime: null }
+ }
+ if (event.type === 'user/message') {
+ const injected = event.data.source.kind === 'plugin' && event.data.source.plugin === name
+ const withMessage = state.lastMessageTime === event.time
+ ? state
+ : { ...state, lastMessageTime: event.time }
+ if (!injected) return withMessage
+ return {
+ ...withMessage,
+ lastInjectionTime: event.time,
+ lastTurnInjectionTime: event.time,
+ }
+ }
+ if (event.type === 'assistant/message' || event.type === 'tool/result') {
+ return state.lastMessageTime === event.time ? state : { ...state, lastMessageTime: event.time }
+ }
+ return state
+ },
+ })
+
ctx.on('agent/pre-step', async (
{ agent, turn, step, signal },
next,
@@ -174,15 +182,17 @@ export function apply(ctx: Context, config: Config): void {
const decision = await next()
if (decision.kind === 'reject' || signal.aborted) return decision
const now = Date.now()
+ const state = ctx.sessionProjections.stateOf(agent.session, 'timeContext') as TimeContextProjection
if (refreshIntervalMs !== undefined && refreshIntervalMs > 0) {
- const lastInjection = latestInjectionTime(agent)
- if (lastInjection !== undefined
+ const lastInjection = state.lastInjectionTime
+ if (lastInjection != null
&& now >= lastInjection
&& now - lastInjection < refreshIntervalMs) return decision
}
+ /* v8 ignore next 6 -- every later step follows a recorded injection in the same turn */
const previous = step === 1
- ? precedingMessageTime(agent)
- : precedingStepContextTime(agent, turn)
+ ? state.lastMessageTime ?? undefined
+ : state.lastTurnInjectionTime ?? undefined
const messages = requestMessages(agent, turn, decision.messages)
const browser = deriveBrowserTimeZoneContext(messages)
const selectedTimeZone = browser.kind === 'resolved' ? browser.timeZone : fallbackTimeZone
diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts
index 7952b95131..7f3aa63e17 100644
--- a/packages/context/time-context/tests/time-context.spec.ts
+++ b/packages/context/time-context/tests/time-context.spec.ts
@@ -7,6 +7,7 @@ import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import AgentRegistry, { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import * as timeContext from '@deepseek-ai/dsh-time-context'
import type { Config } from '@deepseek-ai/dsh-time-context'
@@ -30,6 +31,7 @@ afterEach(() => {
async function mount(config: Config = {}) {
const ctx = new Context()
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentRegistry)
const fiber = await ctx.plugin(timeContext, config)
return { ctx, fiber }
@@ -137,6 +139,7 @@ class ScriptedAdapter extends LlmAdapter {
async function loopHarness(adapter: ScriptedAdapter, config: Config = {}): Promise {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(timeContext, config)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -373,6 +376,7 @@ describe('configuration and lifecycle', () => {
it('fails loud for an invalid explicit zone or an unavailable process zone', async () => {
const invalid = new Context()
+ await invalid.plugin(SessionProjectionRegistry)
await invalid.plugin(AgentRegistry)
await expect(invalid.plugin(timeContext, { timeZone: 'Not/A_Real_Zone' })).rejects.toThrow(
/invalid IANA timeZone/,
@@ -382,6 +386,7 @@ describe('configuration and lifecycle', () => {
throw new RangeError('system zone unavailable')
})
const unresolved = new Context()
+ await unresolved.plugin(SessionProjectionRegistry)
await unresolved.plugin(AgentRegistry)
await expect(unresolved.plugin(timeContext, {})).rejects.toThrow(/failed to resolve the system time zone/)
})
@@ -409,6 +414,33 @@ describe('configuration and lifecycle', () => {
})
})
+describe('time-context projection fold edges', () => {
+ it('clears the open-turn injection time at the next turn start', async () => {
+ const { ctx } = await mount()
+ const session = Session.create(SessionId('same-turn'))
+ session.append('turn/start', { turn: 1 })
+ session.append('user/message', createUserMessage({
+ content: [{ type: 'text', text: 'reading' }],
+ source: { kind: 'plugin', plugin: 'time-context' },
+ }), { surfaceOp: 'append' })
+ expect(typeof ctx.sessionProjections.stateOf(session, 'timeContext')?.lastTurnInjectionTime).toBe('number')
+ session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
+ session.append('turn/start', { turn: 2 })
+ expect(ctx.sessionProjections.stateOf(session, 'timeContext')).toMatchObject({
+ lastTurnInjectionTime: null,
+ })
+ })
+
+ it('keeps the open-turn injection time null when turn/end arrives first', async () => {
+ const { ctx } = await mount()
+ const session = Session.create(SessionId('end-without-start'))
+ session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
+ expect(ctx.sessionProjections.stateOf(session, 'timeContext')).toMatchObject({
+ lastTurnInjectionTime: null,
+ })
+ })
+})
+
describe('real agent-loop request history', () => {
it.each([
['throws'],
@@ -485,11 +517,12 @@ describe('real Loader export path', () => {
const unwrapped = loader.unwrapExports(timeContext) as Record
expect(unwrapped).toBe(timeContext)
expect(unwrapped.name).toBe('time-context')
- expect(unwrapped.inject).toEqual(['agents'])
+ expect(unwrapped.inject).toEqual(['agents', 'sessionProjections'])
expect(unwrapped.Config).toBeDefined()
expect(typeof unwrapped.apply).toBe('function')
const ctx = new Context()
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentRegistry)
const plugin = loader.unwrapExports(timeContext) as Parameters[0]
await ctx.plugin(plugin)
diff --git a/packages/context/time-context/tsconfig.json b/packages/context/time-context/tsconfig.json
index ad66ca2a15..bb6a7d6927 100644
--- a/packages/context/time-context/tsconfig.json
+++ b/packages/context/time-context/tsconfig.json
@@ -4,7 +4,9 @@
"rootDir": "src",
"outDir": "lib/types"
},
- "include": ["src"],
+ "include": [
+ "src"
+ ],
"references": [
{
"path": "../../../vendor/cosmokit"
@@ -35,6 +37,9 @@
},
{
"path": "../../core/session"
+ },
+ {
+ "path": "../../session/session-projection"
}
]
}
diff --git a/packages/context/tmux-context/package.json b/packages/context/tmux-context/package.json
index e513897388..b90a6aa230 100644
--- a/packages/context/tmux-context/package.json
+++ b/packages/context/tmux-context/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tmux-context",
"description": "Opt-in durable per-step context with this agent's tmux pane and window location",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -32,14 +32,16 @@
],
"license": "MIT",
"dependencies": {
- "@deepseek-ai/schemastery": "workspace:^"
+ "@deepseek-ai/schemastery": "workspace:^",
+ "zod": "^4.4.3"
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-shell": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -48,6 +50,7 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/context/tmux-context/src/index.ts b/packages/context/tmux-context/src/index.ts
index 0425bc69de..bfcec10247 100644
--- a/packages/context/tmux-context/src/index.ts
+++ b/packages/context/tmux-context/src/index.ts
@@ -20,7 +20,9 @@
import type { Context, LoggerService } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
-import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
+import { z as zod } from 'zod'
+import type { PreStepDecision } from '@deepseek-ai/dsh-agent'
+import type {} from '@deepseek-ai/dsh-session-projection'
import type { ShellExecutor, ShellRunResult } from '@deepseek-ai/dsh-shell'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
@@ -28,7 +30,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
export const name = 'tmux-context'
/** The agent registry that owns pre-step processing. */
-export const inject = ['agents']
+export const inject = ['agents', 'sessionProjections']
/** Per-turn tmux-location scheduling. Invalid values fail plugin load. */
export interface Config {
@@ -178,21 +180,6 @@ function renderReading(location: TmuxLocation, turn: number): string {
* schedule survives compaction and resumed processes without process-local
* cache state.
*/
-function latestInjectedState(agent: Agent): { state: string; time: number } | undefined {
- for (const event of [...agent.session.events].reverse()) {
- if (event.type === 'user/message'
- && event.data.source.kind === 'plugin'
- && event.data.source.plugin === name) {
- const [block] = event.data.content
- if (block?.type !== 'text') return undefined
- const newline = block.text.indexOf('\n')
- const state = newline === -1 ? '' : block.text.slice(newline + 1)
- return { state, time: event.time }
- }
- }
- return undefined
-}
-
/** Reject refresh intervals that cannot represent an exact elapsed-millisecond threshold. */
function validateRefreshInterval(refreshIntervalMs: number | undefined): void {
if (refreshIntervalMs !== undefined && (
@@ -205,16 +192,47 @@ function validateRefreshInterval(refreshIntervalMs: number | undefined): void {
}
}
+const tmuxContextStateSchema = zod.object({
+ state: zod.string(),
+ time: zod.number(),
+}).nullable()
+
+type TmuxContextState = zod.infer
+
/**
* Register a prepended pre-step listener for the lifetime of `ctx`.
* @param ctx - plugin context; the listener is disposed with it.
* @param config - durable refresh scheduling configuration.
* @throws when the refresh interval is invalid.
*/
+declare module '@deepseek-ai/dsh-session-projection/types' {
+ interface SessionProjectionStateMap {
+ /** The stable state block of this plugin's latest durable injection, or null. */
+ tmuxContext: TmuxContextState
+ }
+}
+
export function apply(ctx: Context, config: Config): void {
const refreshIntervalMs = config.refreshIntervalMs
validateRefreshInterval(refreshIntervalMs)
+ ctx.sessionProjections.register({
+ key: 'tmuxContext',
+ stateVersion: 1,
+ stateSchema: tmuxContextStateSchema,
+ init: () => null,
+ apply: (state, event) => {
+ if (event.type !== 'user/message'
+ || event.data.source.kind !== 'plugin'
+ || event.data.source.plugin !== name) return state
+ const [block] = event.data.content
+ if (block?.type !== 'text') return state
+ const newline = block.text.indexOf('\n')
+ const stableState = newline === -1 ? '' : block.text.slice(newline + 1)
+ return { state: stableState, time: event.time }
+ },
+ })
+
ctx.on('agent/pre-step', async (
{ agent, turn, step, signal },
next,
@@ -223,15 +241,15 @@ export function apply(ctx: Context, config: Config): void {
if (decision.kind === 'reject' || signal.aborted || step !== 1) return decision
const bash = ctx.get('shell')
if (bash === undefined) return decision
- const previous = latestInjectedState(agent)
- if (refreshIntervalMs !== undefined && refreshIntervalMs > 0 && previous !== undefined) {
+ const previous = ctx.sessionProjections.stateOf(agent.session, 'tmuxContext') as TmuxContextState
+ if (refreshIntervalMs !== undefined && refreshIntervalMs > 0 && previous !== null) {
const now = Date.now()
if (now >= previous.time && now - previous.time < refreshIntervalMs) return decision
}
const location = await queryTmuxLocation(bash, ctx.logger, process.pid, signal)
if (location === undefined) return decision
const state = renderState(location)
- if (previous !== undefined && previous.state === state) return decision
+ if (previous !== null && previous.state === state) return decision
const text = renderReading(location, turn)
return {
...decision,
diff --git a/packages/context/tmux-context/tests/tmux-context.spec.ts b/packages/context/tmux-context/tests/tmux-context.spec.ts
index 94b058b2d1..af6de0f6b6 100644
--- a/packages/context/tmux-context/tests/tmux-context.spec.ts
+++ b/packages/context/tmux-context/tests/tmux-context.spec.ts
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import AgentRegistry, { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { ShellExecutor } from '@deepseek-ai/dsh-shell'
import type { ShellExecRequest, ShellExecSpec, ShellProcess, ShellRunResult } from '@deepseek-ai/dsh-shell'
@@ -82,6 +83,7 @@ async function mount(
): Promise<{ ctx: Context; bash: FakeBash | undefined }> {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
let bash: FakeBash | undefined
if (withBash) {
await ctx.plugin(FakeBash)
diff --git a/packages/context/tmux-context/tsconfig.json b/packages/context/tmux-context/tsconfig.json
index 2dda9e0f8a..42fa931fa2 100644
--- a/packages/context/tmux-context/tsconfig.json
+++ b/packages/context/tmux-context/tsconfig.json
@@ -4,7 +4,9 @@
"rootDir": "src",
"outDir": "lib/types"
},
- "include": ["src"],
+ "include": [
+ "src"
+ ],
"references": [
{
"path": "../../../vendor/cosmokit"
@@ -32,6 +34,9 @@
},
{
"path": "../../core/session"
+ },
+ {
+ "path": "../../session/session-projection"
}
]
}
diff --git a/packages/core/agent-default-model/package.json b/packages/core/agent-default-model/package.json
index 6b2b5ef87a..d236420235 100644
--- a/packages/core/agent-default-model/package.json
+++ b/packages/core/agent-default-model/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-default-model",
"description": "Default model selection shared by Agent entry points",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json
index ca3961c893..f9ef271a98 100644
--- a/packages/core/agent-loop/package.json
+++ b/packages/core/agent-loop/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-loop",
"description": "The concrete agent loop plugin for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -37,13 +37,15 @@
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
+ "@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^",
- "@deepseek-ai/dsh-settings": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
- "@deepseek-ai/schemastery": "workspace:^"
+ "@deepseek-ai/schemastery": "workspace:^",
+ "zod": "^4.4.3"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -53,9 +55,10 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
+ "@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^",
- "@deepseek-ai/dsh-settings": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^"
}
}
diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts
index 0d3af9663b..f1377c1ac0 100644
--- a/packages/core/agent-loop/src/agent.ts
+++ b/packages/core/agent-loop/src/agent.ts
@@ -31,6 +31,7 @@ import type { EpochHeader, RequestContext, Session, SessionId, TurnEndReason, Us
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
import { joinContextSections, renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
+import type {} from '@deepseek-ai/dsh-session-projection'
import type { Context } from '@deepseek-ai/cordis'
import { RuntimeContextProjection } from './runtime-context.ts'
import { executeToolCalls } from './tool-calls.ts'
@@ -96,7 +97,8 @@ export class ReactLoopAgent implements Agent {
discarded: (message) => { this.dispatch.emit('agent/inbox/discarded', { message }) },
claimed: (message, turn) => { this.dispatch.emit('agent/inbox/claimed', { message, turn }) },
})
- const lastTurn = session.events.findLast(event => event.type === 'turn/start')?.data.turn ?? 0
+ /* v8 ignore next -- the loop registers its own turnBoundary unit, so the key is always present */
+ const lastTurn = this.loopCtx.sessionProjections.stateOf(session, 'turnBoundary')?.lastTurn ?? 0
this.phase = { kind: 'idle', lastTurn }
this.scope = createScope(loopCtx, this)
this.ctx = this.scope.ctx.extend({ agent: this })
diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts
index 38ee7dfef5..0f0f4858c7 100644
--- a/packages/core/agent-loop/src/index.ts
+++ b/packages/core/agent-loop/src/index.ts
@@ -8,6 +8,7 @@
import { Context, FiberState, Service } from '@deepseek-ai/cordis'
import { randomUUID } from 'node:crypto'
import z from '@deepseek-ai/schemastery'
+import { z as zod } from 'zod'
import { emitAgentEvent } from '@deepseek-ai/dsh-agent'
import type {
Agent,
@@ -18,6 +19,7 @@ import type {
CreateAgentOptions,
ResumeAgentOptions,
SessionStartSource,
+ TurnBoundaryProjection,
} from '@deepseek-ai/dsh-agent'
import { errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
@@ -25,6 +27,8 @@ import { SessionId, SessionPreparation } from '@deepseek-ai/dsh-session'
import type { Session, SessionHeader } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-tools'
+import type {} from '@deepseek-ai/dsh-session-projection'
+import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
import { ReactLoopAgent } from './agent.ts'
import { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from './constants.ts'
@@ -36,6 +40,57 @@ const INACTIVE_STATES: ReadonlySet = new Set([
FiberState.FAILED,
])
+const turnBoundaryProjectionSchema: zod.ZodType = zod.object({
+ openTurnStartSeq: zod.number().int().nonnegative().nullable(),
+ lastStepStartSeq: zod.number().int().nonnegative().nullable(),
+ lastStepBoundary: zod.object({
+ kind: zod.union([zod.literal('start'), zod.literal('end')]),
+ seq: zod.number().int().nonnegative(),
+ }).nullable(),
+ lastTurn: zod.number().int().nonnegative(),
+})
+
+/** Host projection of agent turn and step boundaries. */
+export const turnBoundaryProjectionDefinition = {
+ key: 'turnBoundary',
+ stateVersion: 2,
+ stateSchema: turnBoundaryProjectionSchema,
+ init: () => ({
+ openTurnStartSeq: null,
+ lastStepStartSeq: null,
+ lastStepBoundary: null,
+ lastTurn: 0,
+ }),
+ apply: (state, event) => {
+ switch (event.type) {
+ case 'turn/start':
+ return {
+ ...state,
+ openTurnStartSeq: event.seq,
+ lastTurn: event.data.turn,
+ }
+ case 'turn/end':
+ return {
+ ...state,
+ openTurnStartSeq: null,
+ }
+ case 'step/start':
+ return {
+ ...state,
+ lastStepStartSeq: event.seq,
+ lastStepBoundary: { kind: 'start', seq: event.seq },
+ }
+ case 'step/end':
+ return {
+ ...state,
+ lastStepBoundary: { kind: 'end', seq: event.seq },
+ }
+ default:
+ return state
+ }
+ },
+} satisfies ProjectionDefinition<'turnBoundary', TurnBoundaryProjection>
+
/** Factory-level ownership: live agent teardowns plus config startup work. */
class FactoryOwnership {
private accepting = true
@@ -294,7 +349,7 @@ function validateConfiguredAgents(agents: Config['agents']): void {
/** Concrete agent factory and driver service. */
export class AgentLoop extends Service implements AgentFactory {
- static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
+ static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt', 'sessionProjections']
/** Runtime schema for declarative agents. */
static Config = z.object({
@@ -319,6 +374,7 @@ export class AgentLoop extends Service implements AgentFactory {
constructor(ctx: Context, config: Config) {
super(ctx, 'agentLoop')
+
const entry: AgentLoopSettings = {
maxParallelToolCalls: resolveMaxParallelToolCalls(config.maxParallelToolCalls),
}
@@ -345,6 +401,9 @@ export class AgentLoop extends Service implements AgentFactory {
onChange: () => {},
})
validateConfiguredAgents(this.config.agents)
+ // Register only after every config validation above has passed, so a
+ // rejected constructor leaves no projection unit behind.
+ ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
this.ownership = new FactoryOwnership(ctx.fiber)
this.runtime = { ctx }
ctx.effect(() => () => this.ownership.dispose(), 'agentLoop.transactions()')
diff --git a/packages/core/agent-loop/tests/agent-initiator.spec.ts b/packages/core/agent-loop/tests/agent-initiator.spec.ts
index 70e3d26842..395a0333c2 100644
--- a/packages/core/agent-loop/tests/agent-initiator.spec.ts
+++ b/packages/core/agent-loop/tests/agent-initiator.spec.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { Context, type Fiber } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LlmRuntime, { createUserMessage, ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
@@ -21,6 +22,7 @@ async function harness(adapter: LlmAdapter): Promise {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
const agentsFiber = await ctx.plugin(AgentRegistry)
@@ -121,6 +123,7 @@ describe('AgentLoop initiator scope', () => {
const adapter = new OverlapAdapter(ctx)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -380,6 +383,7 @@ describe('AgentLoop initiator scope', () => {
const adapter = new ReloadAdapter()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/agent.spec.ts b/packages/core/agent-loop/tests/agent.spec.ts
index 29073353e3..fc39e46a28 100644
--- a/packages/core/agent-loop/tests/agent.spec.ts
+++ b/packages/core/agent-loop/tests/agent.spec.ts
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
@@ -13,6 +14,7 @@ async function harness(adapter: MockAdapter): Promise {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/cancel.spec.ts b/packages/core/agent-loop/tests/cancel.spec.ts
index 4a9c6c93d1..4abaf954b8 100644
--- a/packages/core/agent-loop/tests/cancel.spec.ts
+++ b/packages/core/agent-loop/tests/cancel.spec.ts
@@ -15,6 +15,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
function driverDone(agent: Agent): Promise {
@@ -25,6 +26,7 @@ async function harness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -669,6 +671,7 @@ describe('Agent.cancel()', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/config-session-id.spec.ts b/packages/core/agent-loop/tests/config-session-id.spec.ts
index b08c076dad..a19b5da3f5 100644
--- a/packages/core/agent-loop/tests/config-session-id.spec.ts
+++ b/packages/core/agent-loop/tests/config-session-id.spec.ts
@@ -12,6 +12,7 @@ import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop, { CONFIGURED_AGENT_IDENTITIES_KEY } from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
const dirs: string[] = []
@@ -29,6 +30,7 @@ async function makeCoreContext(): Promise {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -327,6 +329,7 @@ describe('config-driven session id', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -352,6 +355,7 @@ describe('config-driven session id', () => {
const ctx1 = new Context()
await ctx1.plugin(LlmRuntime)
await ctx1.plugin(SessionStore)
+ await ctx1.plugin(SessionProjectionRegistry)
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRuntime)
await ctx1.plugin(AgentRegistry)
@@ -371,6 +375,7 @@ describe('config-driven session id', () => {
const ctx2 = new Context()
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
+ await ctx2.plugin(SessionProjectionRegistry)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -395,6 +400,7 @@ describe('config-driven session id', () => {
const ctx1 = new Context()
await ctx1.plugin(LlmRuntime)
await ctx1.plugin(SessionStore)
+ await ctx1.plugin(SessionProjectionRegistry)
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRuntime)
await ctx1.plugin(AgentRegistry)
@@ -411,6 +417,7 @@ describe('config-driven session id', () => {
const ctx2 = new Context()
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
+ await ctx2.plugin(SessionProjectionRegistry)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -436,6 +443,7 @@ describe('config-driven session id', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts
index 25a0c7a85a..e01fd73b01 100644
--- a/packages/core/agent-loop/tests/contract-regressions.spec.ts
+++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts
@@ -6,6 +6,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { defineContentToolFixture, type PostToolDecision } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { ReactLoopAgent } from '../src/agent.ts'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
@@ -30,6 +31,7 @@ async function harness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -525,6 +527,7 @@ describe('turn numbering continues across seeded sessions', () => {
const ctx2 = new Context()
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
+ await ctx2.plugin(SessionProjectionRegistry)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -676,6 +679,7 @@ describe('turn and step boundary recovery', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1108,6 +1112,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1158,6 +1163,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1208,6 +1214,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1254,6 +1261,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1302,6 +1310,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/coverage-edges.spec.ts b/packages/core/agent-loop/tests/coverage-edges.spec.ts
index b42b3789ec..d028451481 100644
--- a/packages/core/agent-loop/tests/coverage-edges.spec.ts
+++ b/packages/core/agent-loop/tests/coverage-edges.spec.ts
@@ -8,6 +8,7 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
function driverDone(agent: Agent): Promise {
@@ -18,6 +19,7 @@ async function harness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/interception.spec.ts b/packages/core/agent-loop/tests/interception.spec.ts
index bc2c381c2a..756d1d7ccc 100644
--- a/packages/core/agent-loop/tests/interception.spec.ts
+++ b/packages/core/agent-loop/tests/interception.spec.ts
@@ -16,6 +16,7 @@ import AgentRegistry, {
} from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
/**
@@ -31,6 +32,7 @@ async function harness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts
index 487fc60181..8f0df876c5 100644
--- a/packages/core/agent-loop/tests/loop.spec.ts
+++ b/packages/core/agent-loop/tests/loop.spec.ts
@@ -7,6 +7,7 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from './mock-adapter.ts'
function driverDone(agent: Agent): Promise {
@@ -17,6 +18,7 @@ async function harness(adapter: MockAdapter, persona = '') {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1466,6 +1468,7 @@ describe('agent loop', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1493,6 +1496,7 @@ describe('agent loop', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/properties.spec.ts b/packages/core/agent-loop/tests/properties.spec.ts
index 2757a633be..38d135abb3 100644
--- a/packages/core/agent-loop/tests/properties.spec.ts
+++ b/packages/core/agent-loop/tests/properties.spec.ts
@@ -20,6 +20,7 @@ import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import fc from 'fast-check'
/** A never-exhausting adapter: every model call returns the same short reply. */
@@ -39,6 +40,7 @@ async function harness() {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/request-cache.e2e.ts b/packages/core/agent-loop/tests/request-cache.e2e.ts
index d47fbccbb1..71633906b3 100644
--- a/packages/core/agent-loop/tests/request-cache.e2e.ts
+++ b/packages/core/agent-loop/tests/request-cache.e2e.ts
@@ -8,6 +8,7 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
/**
@@ -41,6 +42,7 @@ async function loopHarness(): Promise {
const created = new Context()
await created.plugin(LlmRuntime)
await created.plugin(SessionStore)
+ await created.plugin(SessionProjectionRegistry)
await created.plugin(SystemPrompt, { persona: SYSTEM })
await created.plugin(ToolRuntime)
await created.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/request-error.spec.ts b/packages/core/agent-loop/tests/request-error.spec.ts
index 092ede6eb5..2ee77022a5 100644
--- a/packages/core/agent-loop/tests/request-error.spec.ts
+++ b/packages/core/agent-loop/tests/request-error.spec.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LlmRuntime, { createUserMessage, LlmError } from '@deepseek-ai/dsh-llm'
import type { LlmFailure, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
@@ -13,6 +14,7 @@ async function harness(adapter: MockAdapter): Promise {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/request-reconstruction.spec.ts b/packages/core/agent-loop/tests/request-reconstruction.spec.ts
index a53f0e50a5..537ff03df9 100644
--- a/packages/core/agent-loop/tests/request-reconstruction.spec.ts
+++ b/packages/core/agent-loop/tests/request-reconstruction.spec.ts
@@ -15,6 +15,7 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
async function harness(adapter: MockAdapter, persona = 'stable base') {
@@ -28,6 +29,7 @@ async function harnessRoutes(
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -340,6 +342,7 @@ describe('request stability across the loop', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona: 'stable base' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -458,6 +461,7 @@ describe('request stability across the loop', () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona: 'stable base' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/resume.spec.ts b/packages/core/agent-loop/tests/resume.spec.ts
index 1c140e1b2e..df2849d39d 100644
--- a/packages/core/agent-loop/tests/resume.spec.ts
+++ b/packages/core/agent-loop/tests/resume.spec.ts
@@ -13,6 +13,7 @@ import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
const dirs: string[] = []
@@ -28,6 +29,7 @@ async function mountPersistentHarness(root: string, adapter: MockAdapter): Promi
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -244,6 +246,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const ctx2 = new Context()
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
+ await ctx2.plugin(SessionProjectionRegistry)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -272,6 +275,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const ctx2 = new Context()
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
+ await ctx2.plugin(SessionProjectionRegistry)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -522,6 +526,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -585,6 +590,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const ctx2 = new Context()
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
+ await ctx2.plugin(SessionProjectionRegistry)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -617,6 +623,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const ctx2 = new Context()
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
+ await ctx2.plugin(SessionProjectionRegistry)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -653,6 +660,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const ctx2 = new Context()
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
+ await ctx2.plugin(SessionProjectionRegistry)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -686,6 +694,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -875,6 +884,7 @@ describe('configured-start failure edges', () => {
const configured = new Context()
await configured.plugin(LlmRuntime)
await configured.plugin(SessionStore)
+ await configured.plugin(SessionProjectionRegistry)
await configured.plugin(SystemPrompt)
await configured.plugin(ToolRuntime)
await configured.plugin(AgentRegistry)
@@ -920,6 +930,7 @@ describe('configured-start failure edges', () => {
const configured = new Context()
await configured.plugin(LlmRuntime)
await configured.plugin(SessionStore)
+ await configured.plugin(SessionProjectionRegistry)
await configured.plugin(SystemPrompt)
await configured.plugin(ToolRuntime)
await configured.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts
index abe3730fd1..6b78213200 100644
--- a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts
+++ b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts
@@ -10,6 +10,7 @@ import AgentRegistry, { agentEvents, assembleContextFor } from '@deepseek-ai/dsh
import type { Agent } from '@deepseek-ai/dsh-agent'
import { scopeOf } from '@deepseek-ai/dsh-scope'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -17,6 +18,7 @@ async function harnessWithLoop(adapter: MockAdapter = new MockAdapter([textRespo
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona: 'You are the deployment.' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/settings.spec.ts b/packages/core/agent-loop/tests/settings.spec.ts
index 4b45baab44..aba9cc47b8 100644
--- a/packages/core/agent-loop/tests/settings.spec.ts
+++ b/packages/core/agent-loop/tests/settings.spec.ts
@@ -8,6 +8,7 @@ import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry from '@deepseek-ai/dsh-agent'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { SettingsProvider } from '@deepseek-ai/dsh-settings'
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
import AgentLoop, { AGENT_LOOP_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-loop'
@@ -34,6 +35,7 @@ async function boot(): Promise<{ ctx: Context; settingsFiber: Fiber; loopFiber:
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/tool-calls.spec.ts b/packages/core/agent-loop/tests/tool-calls.spec.ts
index f65454949c..e4d2c993e9 100644
--- a/packages/core/agent-loop/tests/tool-calls.spec.ts
+++ b/packages/core/agent-loop/tests/tool-calls.spec.ts
@@ -12,6 +12,7 @@ import LlmRuntime from '@deepseek-ai/dsh-llm'
import ToolRuntime, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH, TOOL_RUNTIME_SCHEDULER, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
@@ -20,6 +21,7 @@ async function harness(adapter: MockAdapter, maxParallelToolCalls?: number) {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -268,6 +270,8 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
})
it('defensively rejects invalid caps when direct construction bypasses the config schema', () => {
+ // Validation precedes the turnBoundary registration, so a rejected
+ // constructor registers nothing and needs no fiber cleanup.
expect(() => new AgentLoop(new Context(), { agents: [], maxParallelToolCalls: 0 }))
.toThrow('maxParallelToolCalls must be a positive integer')
expect(() => new AgentLoop(new Context(), { agents: [], maxParallelToolCalls: 1.5 }))
@@ -278,6 +282,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -346,6 +351,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -690,7 +696,7 @@ describe('tool-call scheduler: failure quiescence', () => {
})
})
-describe('code-mode native-tool denial through the agent loop', () => {
+describe('PTC mode native-tool denial through the agent loop', () => {
/** A minimal in-process code runtime for test purposes — never actually runs. */
class FakeCodeRuntime extends CodeRuntime {
readonly language = 'typescript'
@@ -700,12 +706,13 @@ describe('code-mode native-tool denial through the agent loop', () => {
}
}
- async function codeModeHarness(adapter: MockAdapter) {
+ async function ptcModeHarness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona: '' })
- await ctx.plugin(ToolRuntime, { mode: 'code' })
+ await ctx.plugin(ToolRuntime, { mode: 'ptc' })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- FakeCodeRuntime is an internal test helper with an opaque type shape
await ctx.plugin(FakeCodeRuntime as any)
await ctx.plugin(AgentRegistry)
@@ -714,7 +721,7 @@ describe('code-mode native-tool denial through the agent loop', () => {
return ctx
}
- it('denies a model-direct native-tool call under code mode: tool body never runs and session records UNKNOWN_TOOL', async () => {
+ it('denies a model-direct native-tool call under PTC mode: tool body never runs and session records UNKNOWN_TOOL', async () => {
let toolInvoked = false
const tool = defineContentToolFixture({
name: 'write',
@@ -729,7 +736,7 @@ describe('code-mode native-tool denial through the agent loop', () => {
},
})
- // Scripted model emits a native tool call under code mode — the wire
+ // Scripted model emits a native tool call under PTC mode — the wire
// never advertised it, but a non-compliant provider may still emit one.
const adapter = new MockAdapter([
[
@@ -738,7 +745,7 @@ describe('code-mode native-tool denial through the agent loop', () => {
],
])
- const ctx = await codeModeHarness(adapter)
+ const ctx = await ptcModeHarness(adapter)
ctx.tools.register(tool)
const agent = ctx.agentLoop.create(SessionId('code-native'), { provider: 'mock', model: 'mock' })
diff --git a/packages/core/agent-loop/tests/tool-order.spec.ts b/packages/core/agent-loop/tests/tool-order.spec.ts
index a9af9f56bb..563e5ba6c3 100644
--- a/packages/core/agent-loop/tests/tool-order.spec.ts
+++ b/packages/core/agent-loop/tests/tool-order.spec.ts
@@ -17,12 +17,14 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['toolOrder']) {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt, { persona: 'stable base', ...toolOrder !== undefined ? { toolOrder } : {} })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tsconfig.json b/packages/core/agent-loop/tsconfig.json
index 1765cb00b3..23dbe218ae 100644
--- a/packages/core/agent-loop/tsconfig.json
+++ b/packages/core/agent-loop/tsconfig.json
@@ -43,6 +43,9 @@
},
{
"path": "../../runtime-diagnostics/invariants"
+ },
+ {
+ "path": "../../session/session-projection"
}
]
}
diff --git a/packages/core/agent-tool-presentation/README.i18n.yaml b/packages/core/agent-tool-presentation/README.i18n.yaml
index cc98669766..7f29e5c109 100644
--- a/packages/core/agent-tool-presentation/README.i18n.yaml
+++ b/packages/core/agent-tool-presentation/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-tool-presentation/README.md
-README.md: 5be7a596f6773ea29543758e624f68d0f6be3ffc
-README.zh.md: 52cec9b1958699c15699c8c29cf5f4b3f0f78d91
+README.md: c57938f2b32bead9558b283112c09e8a46b6e866
+README.zh.md: ac511f0793c1f883d539fd05cc53655869f19f5d
diff --git a/packages/core/agent-tool-presentation/README.md b/packages/core/agent-tool-presentation/README.md
index 5be7a596f6..c57938f2b3 100644
--- a/packages/core/agent-tool-presentation/README.md
+++ b/packages/core/agent-tool-presentation/README.md
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
-An [agent preset](../../preset/agent-presets/README.md) carries `dsh-agent-tool-presentation` to choose which form of its tools the model sees: `native` (every visible schema), `code` (only `run_code` plus a generated SDK), or `both`. The tool registry itself stays on the host plane — this row only declares the presentation for the mounting agent, so a Code Mode session runs beside native ones in one process, each seeing its own catalog. A code mode waits for a code runtime before mounting, so a preset selecting Code Mode against a deployment without one fails at mount instead of at the first prompt. The `mode` field is required: a preset without this row already gets the deployment default. Choose it when an agent preset needs to fix the tool form its agents' models see.
+An [agent preset](../../preset/agent-presets/README.md) carries `dsh-agent-tool-presentation` to choose which form of its tools the model sees: `native` (every visible schema), `ptc` (only `run_code` plus a generated SDK), or `both`. The tool registry itself stays on the host plane — this row only declares the presentation for the mounting agent, so a PTC mode session runs beside native ones in one process, each seeing its own catalog. A PTC mode waits for a code runtime before mounting, so a preset selecting PTC mode against a deployment without one fails at mount instead of at the first prompt. The `mode` field is required: a preset without this row already gets the deployment default. Choose it when an agent preset needs to fix the tool form its agents' models see.
## Table of Contents
@@ -25,25 +25,25 @@ An [agent preset](../../preset/agent-presets/README.md) carries `dsh-agent-tool-
## Use this package
-Add this row to an agent preset to fix how every agent joined to that preset sees its tools. `native` presents each visible tool schema as a function definition; `code` presents only the `run_code` transport plus a generated SDK and the rule that only `run_code` may be called directly; `both` presents both forms. Agents that declare nothing get the deployment-wide `mode` on the [`dsh-tools`](../tools/README.md) row.
+Add this row to an agent preset to fix how every agent joined to that preset sees its tools. `native` presents each visible tool schema as a function definition; `ptc` presents only the `run_code` transport plus a generated SDK and the rule that only `run_code` may be called directly; `both` presents both forms. Agents that declare nothing get the deployment-wide `mode` on the [`dsh-tools`](../tools/README.md) row.
### Add the row to a preset
```yaml
- name: '@deepseek-ai/dsh-agent-tool-presentation'
config:
- mode: code
+ mode: ptc
```
| Field | Default | Meaning |
|---|---|---|
-| `mode` | required | `native` — every schema; `code` — `run_code` plus generated SDK; `both` — both forms |
+| `mode` | required | `native` — every schema; `ptc` — `run_code` plus generated SDK; `both` — both forms |
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-agent-tool-presentation) is the exhaustive source for every accepted field. `mode` is required rather than defaulted because a preset without this row inherits the deployment default.
-### What code mode requires
+### What PTC mode requires
-Selecting `code` or `both` needs a composed code runtime (`ctx.codeRuntime`) whose language has a registered SDK renderer — the TypeScript runtime ships via [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md), and both the TypeScript and Python SDK renderers are built into `dsh-tools`. A preset that selects a code mode against a deployment composing no such runtime refuses to mount, naming this row, so the failure lands where the operator can act instead of at the session's first request.
+Selecting `ptc` or `both` needs a composed code runtime (`ctx.codeRuntime`) whose language has a registered SDK renderer — the TypeScript runtime ships via [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md), and both the TypeScript and Python SDK renderers are built into `dsh-tools`. A preset that selects a PTC mode against a deployment composing no such runtime refuses to mount, naming this row, so the failure lands where the operator can act instead of at the session's first request.
### One presentation per agent
@@ -61,7 +61,7 @@ This section explains how the package realizes the behavior above; the observabl
### Design concept
-The tool registry cannot move into a preset: its consumers are all host-plane — the agent loop reads its scheduler, the API proxy reads its presenters, and every tool plugin registers into it — and a service only moves down when all of its consumers move with it. What a preset can own is the presentation of that registry. `ctx.tools.presentAs()` declares it for the mounting scope, which is the preset's standing mount, so the declaration covers every agent joined to that preset and a Code Mode preset runs beside native ones in one process. One row per composition, not one per session.
+The tool registry cannot move into a preset: its consumers are all host-plane — the agent loop reads its scheduler, the API proxy reads its presenters, and every tool plugin registers into it — and a service only moves down when all of its consumers move with it. What a preset can own is the presentation of that registry. `ctx.tools.presentAs()` declares it for the mounting scope, which is the preset's standing mount, so the declaration covers every agent joined to that preset and a PTC mode preset runs beside native ones in one process. One row per composition, not one per session.
### Source map
@@ -72,7 +72,7 @@ The tool registry cannot move into a preset: its consumers are all host-plane
### Behavior notes
-`native` applies immediately. A code mode instead waits for `ctx.codeRuntime`, a host-plane service: a preset selecting Code Mode against a deployment composing no runtime holds this row pending, and `dsh-agent-presets` refuses the mount naming this id. `presentAs` is itself the effect, so the declaration unwinds with this row without a second wrapper owning it.
+`native` applies immediately. A PTC mode instead waits for `ctx.codeRuntime`, a host-plane service: a preset selecting PTC mode against a deployment composing no runtime holds this row pending, and `dsh-agent-presets` refuses the mount naming this id. `presentAs` is itself the effect, so the declaration unwinds with this row without a second wrapper owning it.
@@ -85,8 +85,8 @@ The package-level contract is enough for most consumers; read these when you nee
- [tools package](../tools/README.md) — the tool presentation modes and `presentAs` API.
- [agent-presets package](../../preset/agent-presets/README.md) — how presets compose agents and their standing mounts.
-- [code-runtime worker-thread package](../../code-runtime/code-runtime-worker-thread/README.md) — the TypeScript runtime a code mode needs.
-- [Code Mode executor-collapse note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md) — why the announced and callable surfaces stay the same.
+- [code-runtime worker-thread package](../../code-runtime/code-runtime-worker-thread/README.md) — the TypeScript runtime a PTC mode needs.
+- [PTC mode executor-collapse note](../../../.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.md) — why the announced and callable surfaces stay the same.
- [Core group map](../README.md) — how the core packages compose.
-----
@@ -107,7 +107,7 @@ No direct invalidation; the presentation is fixed when the agent is composed, so
These limits define when this row needs special care. They are current package constraints, not a task backlog.
-- **The runtime stays host-plane** — a preset can select Code Mode but cannot supply the TypeScript runtime it needs; a deployment that composes none can compose no code-mode preset.
+- **The runtime stays host-plane** — a preset can select PTC mode but cannot supply the TypeScript runtime it needs; a deployment that composes none can compose no ptc preset.
### Dev Note
diff --git a/packages/core/agent-tool-presentation/README.zh.md b/packages/core/agent-tool-presentation/README.zh.md
index 52cec9b195..ac511f0793 100644
--- a/packages/core/agent-tool-presentation/README.zh.md
+++ b/packages/core/agent-tool-presentation/README.zh.md
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
-[agent preset](../../preset/agent-presets/README.zh.md) 携带 `dsh-agent-tool-presentation`,用来声明「模型看到其工具的哪一种形态」:`native`(每个可见 schema)、`code`(只有 `run_code` 加一份生成的 SDK)或 `both`。工具注册表本身仍在宿主平面——这一行只声明挂载 agent 的呈现方式,因此一个 Code Mode 会话可以与多个 native 会话同进程并存,各自看到各自的目录。code 类模式在挂载前会等待代码运行时,因此针对未组装运行时的部署选择 Code Mode 的 preset 会在挂载时失败,而不是在第一次请求时失败。`mode` 字段是必填的:不带这一行的 preset 本来就会拿到部署默认值。当 agent preset 需要固定其 agent 的模型所看到的工具形态时,请选择本包。
+[agent preset](../../preset/agent-presets/README.zh.md) 携带 `dsh-agent-tool-presentation`,用来声明「模型看到其工具的哪一种形态」:`native`(每个可见 schema)、`ptc`(只有 `run_code` 加一份生成的 SDK)或 `both`。工具注册表本身仍在宿主平面——这一行只声明挂载 agent 的呈现方式,因此一个 PTC mode 会话可以与多个 native 会话同进程并存,各自看到各自的目录。PTC 模式在挂载前会等待代码运行时,因此针对未组装运行时的部署选择 PTC mode 的 preset 会在挂载时失败,而不是在第一次请求时失败。`mode` 字段是必填的:不带这一行的 preset 本来就会拿到部署默认值。当 agent preset 需要固定其 agent 的模型所看到的工具形态时,请选择本包。
## 目录
@@ -25,25 +25,25 @@ kind: "package-reference"
## 使用本包
-把这一行加入 agent preset,以固定每个加入该 preset 的 agent 看到其工具的方式。`native` 以函数定义的形式呈现每个可见工具 schema;`code` 只呈现 `run_code` 传输、一份生成的 SDK 以及「只有 `run_code` 可被直接调用」这条规则;`both` 同时呈现两种形态。未作声明的 agent 会拿到 [`dsh-tools`](../tools/README.zh.md) 那一行上的部署级 `mode`。
+把这一行加入 agent preset,以固定每个加入该 preset 的 agent 看到其工具的方式。`native` 以函数定义的形式呈现每个可见工具 schema;`ptc` 只呈现 `run_code` 传输、一份生成的 SDK 以及「只有 `run_code` 可被直接调用」这条规则;`both` 同时呈现两种形态。未作声明的 agent 会拿到 [`dsh-tools`](../tools/README.zh.md) 那一行上的部署级 `mode`。
### 把这一行加入 preset
```yaml
- name: '@deepseek-ai/dsh-agent-tool-presentation'
config:
- mode: code
+ mode: ptc
```
| 字段 | 默认值 | 含义 |
|---|---|---|
-| `mode` | 必填 | `native`——每个 schema;`code`——`run_code` 加生成 SDK;`both`——两种形态 |
+| `mode` | 必填 | `native`——每个 schema;`ptc`——`run_code` 加生成 SDK;`both`——两种形态 |
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-agent-tool-presentation)是每个受支持字段的穷尽式真源。`mode` 是必填而非有默认值,因为不带这一行的 preset 会继承部署默认值。
-### code 模式需要什么
+### PTC 模式需要什么
-选择 `code` 或 `both` 需要已组合的代码运行时(`ctx.codeRuntime`),且其语言有已注册的 SDK 渲染器——TypeScript 运行时经 [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.zh.md) 交付,TypeScript 与 Python 的 SDK 渲染器都内置在 `dsh-tools` 中。针对未组装此类运行时的部署选择 code 类模式的 preset 会拒绝挂载并点名这一行,使失败落在操作者可以行动的地方,而不是落在会话的第一次请求上。
+选择 `ptc` 或 `both` 需要已组合的代码运行时(`ctx.codeRuntime`),且其语言有已注册的 SDK 渲染器——TypeScript 运行时经 [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.zh.md) 交付,TypeScript 与 Python 的 SDK 渲染器都内置在 `dsh-tools` 中。针对未组装此类运行时的部署选择 PTC 模式的 preset 会拒绝挂载并点名这一行,使失败落在操作者可以行动的地方,而不是落在会话的第一次请求上。
### 每个 agent 只声明一次呈现方式
@@ -61,7 +61,7 @@ kind: "package-reference"
### 设计理念
-工具注册表搬不进 preset:它的消费者全在宿主平面——agent loop 读它的调度器,API proxy 读它的 presenter,每个工具插件都往里注册——而一个服务只有在所有消费者一起下沉时才能下沉。preset 能拥有的是这份注册表的呈现方式。`ctx.tools.presentAs()` 为挂载作用域声明它,而挂载作用域就是 preset 的常驻挂载,因此该声明覆盖每个加入该 preset 的 agent,一个 Code Mode preset 可以与多个 native 会话同进程并存。每个组合一行,而不是每个会话一行。
+工具注册表搬不进 preset:它的消费者全在宿主平面——agent loop 读它的调度器,API proxy 读它的 presenter,每个工具插件都往里注册——而一个服务只有在所有消费者一起下沉时才能下沉。preset 能拥有的是这份注册表的呈现方式。`ctx.tools.presentAs()` 为挂载作用域声明它,而挂载作用域就是 preset 的常驻挂载,因此该声明覆盖每个加入该 preset 的 agent,一个 PTC mode preset 可以与多个 native 会话同进程并存。每个组合一行,而不是每个会话一行。
### 源码地图
@@ -72,7 +72,7 @@ kind: "package-reference"
### 行为说明
-`native` 立即生效。code 类模式则等待 `ctx.codeRuntime`——这是一个宿主平面服务:针对未组装运行时的部署选择 Code Mode 的 preset 会让这一行停在 pending,`dsh-agent-presets` 会指名此 id 拒绝挂载。`presentAs` 本身就是 effect,因此该声明随这一行撤销,无需第二个包装层拥有它。
+`native` 立即生效。PTC 模式则等待 `ctx.codeRuntime`——这是一个宿主平面服务:针对未组装运行时的部署选择 PTC mode 的 preset 会让这一行停在 pending,`dsh-agent-presets` 会指名此 id 拒绝挂载。`presentAs` 本身就是 effect,因此该声明随这一行撤销,无需第二个包装层拥有它。
@@ -85,8 +85,8 @@ kind: "package-reference"
- [tools 包](../tools/README.zh.md)——工具呈现模式与 `presentAs` API。
- [agent-presets 包](../../preset/agent-presets/README.zh.md)——preset 如何组合 agent 及其常驻挂载。
-- [code-runtime worker-thread 包](../../code-runtime/code-runtime-worker-thread/README.zh.md)——code 模式所需的 TypeScript 运行时。
-- [Code Mode 执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.zh.md)——通告面与可调用面为何保持一致。
+- [code-runtime worker-thread 包](../../code-runtime/code-runtime-worker-thread/README.zh.md)——PTC 模式所需的 TypeScript 运行时。
+- [PTC mode 执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.zh.md)——通告面与可调用面为何保持一致。
- [core 分组地图](../README.zh.md)——core 各包如何组合。
-----
@@ -107,7 +107,7 @@ kind: "package-reference"
这些限制说明这一行何时需要特别留意。它们是当前包约束,不是任务积压。
-- **运行时仍在宿主平面**——preset 可以选择 Code Mode,却无法自带它所需的 TypeScript 运行时;未组装运行时的部署也就无法组装任何 code 模式的 preset。
+- **运行时仍在宿主平面**——preset 可以选择 PTC mode,却无法自带它所需的 TypeScript 运行时;未组装运行时的部署也就无法组装任何 PTC 模式的 preset。
### 开发备注
diff --git a/packages/core/agent-tool-presentation/package.json b/packages/core/agent-tool-presentation/package.json
index 382c0caf61..51ef164ac2 100644
--- a/packages/core/agent-tool-presentation/package.json
+++ b/packages/core/agent-tool-presentation/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-tool-presentation",
- "description": "Agent-plane presentation selector: composes one agent's tools as Code Mode, native, or both",
- "version": "0.1.1-rc.2",
+ "description": "Agent-plane presentation selector: composes one agent's tools as PTC mode, native, or both",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/core/agent-tool-presentation/src/index.ts b/packages/core/agent-tool-presentation/src/index.ts
index c4105f1ec2..fb4880a428 100644
--- a/packages/core/agent-tool-presentation/src/index.ts
+++ b/packages/core/agent-tool-presentation/src/index.ts
@@ -7,13 +7,13 @@
* consumers, so it cannot move into a preset. What a preset CAN own is the
* presentation: `ctx.tools.presentAs()` declares it for the mounting SCOPE,
* which is the preset's standing mount, so the declaration covers every agent
- * joined to that preset and a Code Mode preset runs beside native ones in one
+ * joined to that preset and a PTC mode preset runs beside native ones in one
* process. One row per composition, not one per session.
*
- * A code mode needs a TypeScript code runtime, which is a host-plane service
+ * A PTC mode needs a TypeScript code runtime, which is a host-plane service
* ([`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker/README.md)).
* This row therefore waits for it rather than assuming it: a preset selecting
- * Code Mode against a deployment that composes no runtime fails at mount, named
+ * PTC mode against a deployment that composes no runtime fails at mount, named
* in the preset's own activation audit, instead of at the first prompt.
* @module @deepseek-ai/dsh-agent-tool-presentation
*/
@@ -38,7 +38,7 @@ export const inject = ['tools']
export interface Config {
/**
* The form this agent's model sees. `native` sends every visible schema,
- * `code` sends only `run_code` plus a generated SDK, `both` sends both.
+ * `ptc` sends only `run_code` plus a generated SDK, `both` sends both.
* Required rather than defaulted: the deployment default is what a preset
* without this row already gets, so an omitted value would mean the row was
* composed for nothing.
@@ -48,7 +48,7 @@ export interface Config {
/** Runtime schema. */
export const Config: z = z.object({
- mode: z.union(['native', 'code', 'both'] as const).required(),
+ mode: z.union(['native', 'ptc', 'both'] as const).required(),
})
/**
diff --git a/packages/core/agent-tool-presentation/tests/agent-tool-presentation.spec.ts b/packages/core/agent-tool-presentation/tests/agent-tool-presentation.spec.ts
index 13505fbbb4..3110036e26 100644
--- a/packages/core/agent-tool-presentation/tests/agent-tool-presentation.spec.ts
+++ b/packages/core/agent-tool-presentation/tests/agent-tool-presentation.spec.ts
@@ -63,9 +63,9 @@ describe('the tool-presentation row', () => {
expect(inject).toEqual(['tools'])
})
- it('gives its own agent Code Mode and leaves the rest native', async () => {
+ it('gives its own agent PTC mode and leaves the rest native', async () => {
const ctx = await host()
- const coded = await mount(ctx, { mode: 'code' }, 'coded')
+ const coded = await mount(ctx, { mode: 'ptc' }, 'coded')
const plain = await mount(ctx, { mode: 'native' }, 'plain')
const codedAssembly = await ctx.systemPrompt.assemble({ scope: coded.agent })
@@ -87,7 +87,7 @@ describe('the tool-presentation row', () => {
it('restores the deployment default when the agent unloads', async () => {
const ctx = await host()
- const { agent, row } = await mount(ctx, { mode: 'code' })
+ const { agent, row } = await mount(ctx, { mode: 'ptc' })
await row.dispose()
@@ -101,7 +101,7 @@ describe('the tool-presentation row', () => {
it('waits for a code runtime the deployment does not compose', async () => {
const ctx = await host({ runtime: false })
- const { agent, row } = await mount(ctx, { mode: 'code' })
+ const { agent, row } = await mount(ctx, { mode: 'ptc' })
// Pending, not applied: `dsh-agent-presets` rejects a mount holding a row
// that never reached a usable state, naming this id — so the preset fails
@@ -113,7 +113,7 @@ describe('the tool-presentation row', () => {
it('applies once the runtime arrives', async () => {
const ctx = await host({ runtime: false })
- const { agent } = await mount(ctx, { mode: 'code' })
+ const { agent } = await mount(ctx, { mode: 'ptc' })
await ctx.plugin(StubRuntime)
diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json
index 115feb18aa..31c9ffe81a 100644
--- a/packages/core/agent/package.json
+++ b/packages/core/agent/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent",
"description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -43,7 +43,8 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
@@ -53,6 +54,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts
index 5334728416..ef7371231b 100644
--- a/packages/core/agent/src/index.ts
+++ b/packages/core/agent/src/index.ts
@@ -17,6 +17,7 @@ import type { AgentOptions } from './runtime-types.ts'
export * from './runtime-types.ts'
export * from './types.ts'
+export type * from './projection.ts'
export * from './inbox.ts'
export * from './consumed-work.ts'
export * from './model-selection.ts'
diff --git a/packages/core/agent/src/projection.ts b/packages/core/agent/src/projection.ts
new file mode 100644
index 0000000000..89c10ac858
--- /dev/null
+++ b/packages/core/agent/src/projection.ts
@@ -0,0 +1,11 @@
+import type { TurnBoundaryProjection } from './types.ts'
+import type {} from '@deepseek-ai/dsh-session-projection'
+
+declare module '@deepseek-ai/dsh-session-projection/types' {
+ interface SessionProjectionStateMap {
+ /** The agent session's open/last turn and step boundary facts (whole value). */
+ turnBoundary: TurnBoundaryProjection
+ }
+}
+
+export {}
diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts
index 549af341db..e570ccf7e7 100644
--- a/packages/core/agent/src/types.ts
+++ b/packages/core/agent/src/types.ts
@@ -28,6 +28,26 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
/** One of the two ordered pending-message lists owned by an agent. */
export type InboxTarget = 'next-turn' | 'next-step'
+/**
+ * Turn and step boundaries folded from one agent session log.
+ *
+ * Reader contract: the key is registered by `dsh-agent-loop` and absent
+ * otherwise. Without agent-loop no turn events exist, so readers treat an
+ * absent key as "no open turn / no boundaries" — capability absence, not a
+ * corrupt state. A reader whose behavior has no safe fallback for that
+ * absence (the step-open decision, for example) may fail loud instead.
+ */
+export interface TurnBoundaryProjection {
+ /** Seq of the open turn's `turn/start`, or null between turns. */
+ readonly openTurnStartSeq: number | null
+ /** Seq of the latest `step/start` event, or null before the first step. */
+ readonly lastStepStartSeq: number | null
+ /** The latest step boundary (`step/start` or `step/end`) and its seq, or null before the first step boundary. */
+ readonly lastStepBoundary: { readonly kind: 'start' | 'end'; readonly seq: number } | null
+ /** Turn number of the latest `turn/start`; 0 before the first turn. */
+ readonly lastTurn: number
+}
+
declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
/**
diff --git a/packages/core/agent/tsconfig.json b/packages/core/agent/tsconfig.json
index dfa66f16ff..f7aae0283e 100644
--- a/packages/core/agent/tsconfig.json
+++ b/packages/core/agent/tsconfig.json
@@ -29,6 +29,9 @@
{
"path": "../../runtime-diagnostics/invariants"
},
+ {
+ "path": "../../session/session-projection"
+ },
{
"path": "../../typert/protocol"
}
diff --git a/packages/core/scope/package.json b/packages/core/scope/package.json
index 67b7fc8cc5..b14fa0c9e9 100644
--- a/packages/core/scope/package.json
+++ b/packages/core/scope/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-scope",
"description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/core/scope/src/scoped-events.generated.ts b/packages/core/scope/src/scoped-events.generated.ts
index da93075512..e7f24ca4cb 100644
--- a/packages/core/scope/src/scoped-events.generated.ts
+++ b/packages/core/scope/src/scoped-events.generated.ts
@@ -29,10 +29,10 @@ const scopedSubjectResolvers: Readonly (args[1] as Record)['scope'],
- 'tools/code-dispatch-log': args => (args[0] as Record)['agent'],
'tools/execute': args => (args[0] as Record)['agent'],
'tools/post-execute': args => (args[0] as Record)['agent'],
'tools/pre-execute': args => (args[0] as Record)['agent'],
+ 'tools/ptc-dispatch-log': args => (args[0] as Record)['agent'],
'tools/result': args => (args[0] as Record)['agent'],
'user-questions/request': args => (args[0] as Record)['agent'],
})
diff --git a/packages/core/scope/tests/invariant.spec.ts b/packages/core/scope/tests/invariant.spec.ts
index 96c1488ea2..2adc3d787b 100644
--- a/packages/core/scope/tests/invariant.spec.ts
+++ b/packages/core/scope/tests/invariant.spec.ts
@@ -74,7 +74,7 @@ describe('scoped-dispatch invariants', () => {
['approval/request', [{ agent, toolName: 'echo' }, () => Promise.resolve('unavailable')]],
['goal/changed', [{ agent, change: { operation: 'create', ref: { id: 'goal-a', revision: 1 } } }]],
['system-prompt/assemble', [[], { scope: agent }]],
- ['tools/code-dispatch-log', [{ exec: { callId: 'c', name: 't', arguments: {} }, agent, subCallId: 'c:code:1', name: 't', isError: false, content: [] }, () => Promise.resolve([])]],
+ ['tools/ptc-dispatch-log', [{ exec: { callId: 'c', name: 't', arguments: {} }, agent, subCallId: 'c:code:1', name: 't', isError: false, content: [] }, () => Promise.resolve([])]],
['tools/execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ content: [], isError: false })]],
['tools/post-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, { content: [], isError: false }, () => Promise.resolve({ kind: 'accept' })]],
['tools/pre-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ kind: 'allow' })]],
diff --git a/packages/core/session/package.json b/packages/core/session/package.json
index f1ba6dd289..76062c71a3 100644
--- a/packages/core/session/package.json
+++ b/packages/core/session/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-session",
"description": "Event-sourced session store for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/core/system-prompt/package.json b/packages/core/system-prompt/package.json
index 6b09b326d7..584aa1b0b3 100644
--- a/packages/core/system-prompt/package.json
+++ b/packages/core/system-prompt/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-system-prompt",
"description": "System prompt assembly registry for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts
index c4b5f81ad5..f0f0715971 100644
--- a/packages/core/system-prompt/src/index.ts
+++ b/packages/core/system-prompt/src/index.ts
@@ -134,7 +134,7 @@ export const FIRST_PARTY_SECTION_ORDER = {
DEPLOYMENT_PERSONA: 0,
PLAN_POLICY: 500,
TEAM_POLICY: 600,
- CODE_ONLY: 800,
+ PTC_ONLY: 800,
FILE_REFERENCE: 900,
TOOL_BASH: 1000,
TOOL_PWSH: 1010,
diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml
index 3c177a036c..feacbdc2e4 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: ed62ac8314e3bbd3720921738b9cbe3b0ee7315a
-README.zh.md: 1c42cbffe27bf2affdf7818085896c6d70a5655c
+README.md: f586b926ec9a8e426f48b62e4d03b1661201a76a
+README.zh.md: 5f422dd848ee9a6e8e0afe96d3dd03238ecc3857
diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md
index ed62ac8314..f586b926ec 100644
--- a/packages/core/tools/README.md
+++ b/packages/core/tools/README.md
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
-With `dsh-tools`, tool plugins register schemas and executors, and every model tool call runs through a guarded pipeline — allow/deny/ask policy, monotonic guards, around-dispatch wrappers, result inspection, definition-owned content finalization, and a final observe-only notification. The package also controls how tools are presented to the model: its `mode` config selects native function calling, [Code Mode](#code-mode), or both, and one agent shadows that default for itself with `presentAs`. Tool authors use `defineTool` for typed parameter and output schemas, an optional cooperative timeout, parallel-safety classification, and optional UI presentation intents. Choose it as the registry for any capability you want the model to reach — schemas flow into prompt assembly automatically.
+With `dsh-tools`, tool plugins register schemas and executors, and every model tool call runs through a guarded pipeline — allow/deny/ask policy, monotonic guards, around-dispatch wrappers, result inspection, definition-owned content finalization, and a final observe-only notification. The package also controls how tools are presented to the model: its `mode` config selects native function calling, [PTC mode](#ptc-mode), or both, and one agent shadows that default for itself with `presentAs`. Tool authors use `defineTool` for typed parameter and output schemas, an optional cooperative timeout, parallel-safety classification, and optional UI presentation intents. Choose it as the registry for any capability you want the model to reach — schemas flow into prompt assembly automatically.
## Table of Contents
@@ -61,7 +61,7 @@ The unified schema DSL supports `string`, `number`, `integer`, `boolean`, `null`
### Configure the presentation mode
-The `mode` config decides what the model sees: `native` (every visible schema), `code` (only `run_code` plus a generated SDK), or `both`.
+The `mode` config decides what the model sees: `native` (every visible schema), `ptc` (only `run_code` plus a generated SDK), or `both`.
```yaml
- name: '@deepseek-ai/dsh-tools'
@@ -71,7 +71,7 @@ The `mode` config decides what the model sees: `native` (every visible schema),
| Field | Default | Meaning |
|---|---|---|
-| `mode` | `native` | How visible tools are presented to the model: `native`, `code`, or `both` |
+| `mode` | `native` | How visible tools are presented to the model: `native`, `ptc`, or `both` |
| `maxParallelSubCalls` | `10` | Concurrency cap for a `run_code` program's overlapping sub-calls; `1` restores strictly serial dispatch |
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tools) is the exhaustive source for every accepted field. Non-native modes require a composed `ctx.codeRuntime` whose language has a registered SDK renderer; an agent preset selects its own presentation with [`dsh-agent-tool-presentation`](../agent-tool-presentation/README.md), and one agent can shadow the default with `presentAs(mode)`.
@@ -111,7 +111,7 @@ The registry holds typed `ToolDefinition`s in scoped layers and projects them on
| [`src/schema.ts`](src/schema.ts) | The `defineTool` DSL: `ValueSchemaSpec`, `ParameterSchemaSpec`, `InferValue`, `InferArgs` |
| [`src/json-schema.ts`](src/json-schema.ts) | The enforced raw JSON Schema subset and validation |
| [`src/presentation.ts`](src/presentation.ts) | The `card`-tagged UI render intents |
-| [`src/code-mode.ts`](src/code-mode.ts) | Code Mode: SDK generation, `run_code` dispatch bridge, settlement |
+| [`src/ptc.ts`](src/ptc.ts) | PTC mode: SDK generation, `run_code` dispatch bridge, settlement |
| [`src/ts-types.ts`](src/ts-types.ts) | TypeScript SDK type rendering |
| [`src/py-types.ts`](src/py-types.ts) | Python SDK type rendering |
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion |
@@ -120,9 +120,9 @@ The registry holds typed `ToolDefinition`s in scoped layers and projects them on
Each typed invocation materializes and freezes parsed arguments, assigns an opaque correlation token, and runs policy and dispatch. Cancellation is cooperative and quiescent: every tool body receives the caller-owned `exec.signal` and must observe it; cancellation before body invocation is `ABORTED_BEFORE_DISPATCH`, after invocation it replaces only a successful outcome with `ABORTED`. Denials, wrapper failures, tool failures, post-policy failures, and timeout-owned `TOOL_TIMEOUT` remain more specific. Unknown and throwing tools become structured errors (`UNKNOWN_TOOL`), so a call fails without ending the turn.
-### Code Mode
+### PTC mode
-Under `code` or `both`, the registry exposes the reserved `run_code` transport plus a deterministic SDK generated in the loaded runtime's language. Each SDK binding call re-enters the complete tool pipeline with logged correlation to the outer call, scheduled through a per-run pool that reuses the native concurrency contract. Under `code` alone, a model-direct call naming any other visible tool resolves to `UNKNOWN_TOOL` before policy — the announced surface and the callable surface stay the same. Intermediate binding values are execution-local; only the outer `run_code` result has a hard size cap. The [executor-collapse note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md) owns the collapse contract.
+Under `ptc` or `both`, the registry exposes the reserved `run_code` transport plus a deterministic SDK generated in the loaded runtime's language. Each SDK binding call re-enters the complete tool pipeline with logged correlation to the outer call, scheduled through a per-run pool that reuses the native concurrency contract. Under `ptc` alone, a model-direct call naming any other visible tool resolves to `UNKNOWN_TOOL` before policy — the announced surface and the callable surface stay the same. Intermediate binding values are execution-local; only the outer `run_code` result has a hard size cap. The [executor-collapse note](../../../.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.md) owns the collapse contract.
### Extension points
@@ -164,13 +164,13 @@ Fixed per-request cost proportional to the visible definitions. Restrictions tha
Prefix-stable while visible definitions and their order are unchanged. Registration, disposal, or scoped restriction may invalidate reuse from the first changed schema token.
-### Code Mode schema and system prompt
+### PTC mode schema and system prompt
#### What the model sees
-Code Mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools), the SDK instructions below, and the generated exact SDK block for the loaded runtime's language. The TypeScript instructions identify generated declarations as program-only bindings. When the current `bash` parameter schema accepts the example arguments, they also show a complete `run_code` call around `tools.bash(...)`. The `tools:sdk` section uses first-party order 5000. `both` exposes normal schemas and this Code Mode API; under `code` the prompt also carries the `tools:code-only` rule earlier in the first-party order, so the model reads which tools it may call before it reads what each one is for.
+PTC mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools), the SDK instructions below, and the generated exact SDK block for the loaded runtime's language. The TypeScript instructions identify generated declarations as program-only bindings. When the current `bash` parameter schema accepts the example arguments, they also show a complete `run_code` call around `tools.bash(...)`. The `tools:sdk` section uses first-party order 5000. `both` exposes normal schemas and this PTC mode API; under `ptc` the prompt also carries the `tools:ptc-only` rule earlier in the first-party order, so the model reads which tools it may call before it reads what each one is for.
-##### TypeScript Code Mode SDK instructions with bash
+##### TypeScript PTC mode SDK instructions with bash
```markdown
## Writing code for run_code
@@ -191,17 +191,17 @@ Program-only SDK bindings:
#### Token effect
-Fixed per-request cost proportional to the visible definitions. Code Mode trades end-tool schemas for generated SDK text plus one transport schema rather than promising a universal reduction.
+Fixed per-request cost proportional to the visible definitions. PTC mode trades end-tool schemas for generated SDK text plus one transport schema rather than promising a universal reduction.
#### KV Cache effect
-Prefix-stable while the Code Mode selection, generated SDK, transport schema, and visible tool set are unchanged. Mode or filter changes may invalidate reuse from the first changed prompt or schema token.
+Prefix-stable while the PTC mode selection, generated SDK, transport schema, and visible tool set are unchanged. Mode or filter changes may invalidate reuse from the first changed prompt or schema token.
### Tool-call history and results
#### What the model sees
-The loop retains model-emitted arguments and the registry's final content. Any thrown or denied call becomes exactly `Error: `. Code Mode renders the outer program's printed lines and return value, `(run_code completed with no output)` when both are empty, or `Error: code run failed (): ` followed conditionally by `Captured output:` and the captured lines. Inner dispatch events stay log-only, while a successful image-bearing sub-result is appended after the outer result as source-attributed context.
+The loop retains model-emitted arguments and the registry's final content. Any thrown or denied call becomes exactly `Error: `. PTC mode renders the outer program's printed lines and return value, `(run_code completed with no output)` when both are empty, or `Error: code run failed (): ` followed conditionally by `Captured output:` and the captured lines. Inner dispatch events stay log-only, while a successful image-bearing sub-result is appended after the outer result as source-attributed context.
#### Token effect
@@ -222,8 +222,8 @@ These limits define when the registry needs special care. They are current packa
- **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md).
- **Caller-defined subagent and workflow structured outputs remain object-rooted** — this is a consumer-level guard; the shared schema vocabulary and tool outputs support every JSON root.
- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-tool-call-timeout-policy` wrapper.
-- **Code Mode's SDK language follows the one loaded runtime, and a presentation is per agent rather than per tool** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language` has a registered SDK renderer; within one agent no tool can be native-only while another is code-only.
-- **Code Mode intermediate values are execution-local and unbounded by bytes** — they cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap.
+- **PTC mode's SDK language follows the one loaded runtime, and a presentation is per agent rather than per tool** — `mode: ptc`/`both` rejects prompt assembly unless `ctx.codeRuntime.language` has a registered SDK renderer; within one agent no tool can be native-only while another is ptc-only.
+- **PTC mode intermediate values are execution-local and unbounded by bytes** — they cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap.
- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP, because cross-call state would be invisible to the log.
diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md
index 1c42cbffe2..5f422dd848 100644
--- a/packages/core/tools/README.zh.md
+++ b/packages/core/tools/README.zh.md
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
-使用 `dsh-tools`,工具插件注册 schema 与执行器,每次模型工具调用都经过一条受守卫的流水线——允许/拒绝/询问策略、单调守卫、环绕分发包装层、结果检查、由工具定义持有的内容终结,以及最终的仅观测通知。该包还控制工具向模型呈现的方式:`mode` 配置选择原生 Function Calling(函数调用)、[Code Mode](#code-mode) 或两者,单个 agent 可用 `presentAs` 为自己遮蔽该默认值。工具作者使用 `defineTool` 定义类型化参数与输出 schema、可选的协作式超时、并行安全分类与可选的 UI 呈现意图。把任何希望模型触达的能力做成注册表时请选择本包——schema 会自动流入提示词组装。
+使用 `dsh-tools`,工具插件注册 schema 与执行器,每次模型工具调用都经过一条受守卫的流水线——允许/拒绝/询问策略、单调守卫、环绕分发包装层、结果检查、由工具定义持有的内容终结,以及最终的仅观测通知。该包还控制工具向模型呈现的方式:`mode` 配置选择原生 Function Calling(函数调用)、[PTC mode](#ptc-mode) 或两者,单个 agent 可用 `presentAs` 为自己遮蔽该默认值。工具作者使用 `defineTool` 定义类型化参数与输出 schema、可选的协作式超时、并行安全分类与可选的 UI 呈现意图。把任何希望模型触达的能力做成注册表时请选择本包——schema 会自动流入提示词组装。
## 目录
@@ -61,7 +61,7 @@ ctx.tools.register(defineTool({
### 配置呈现模式
-`mode` 配置决定模型看到什么:`native`(每个可见 schema)、`code`(只有 `run_code` 加一份生成 SDK)或 `both`。
+`mode` 配置决定模型看到什么:`native`(每个可见 schema)、`ptc`(只有 `run_code` 加一份生成 SDK)或 `both`。
```yaml
- name: '@deepseek-ai/dsh-tools'
@@ -71,7 +71,7 @@ ctx.tools.register(defineTool({
| 字段 | 默认值 | 含义 |
|---|---|---|
-| `mode` | `native` | 可见工具向模型呈现的方式:`native`、`code` 或 `both` |
+| `mode` | `native` | 可见工具向模型呈现的方式:`native`、`ptc` 或 `both` |
| `maxParallelSubCalls` | `10` | `run_code` 程序重叠子调用的并发上限;`1` 恢复严格串行分发 |
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tools)是每个受支持字段的穷尽式真源。非原生模式要求已组合的 `ctx.codeRuntime` 且其语言有已注册的 SDK 渲染器;agent preset 通过 [`dsh-agent-tool-presentation`](../agent-tool-presentation/README.zh.md) 自行选择呈现方式,单个 agent 可用 `presentAs(mode)` 遮蔽默认值。
@@ -111,7 +111,7 @@ ctx.tools.register(defineTool({
| [`src/schema.ts`](src/schema.ts) | `defineTool` DSL:`ValueSchemaSpec`、`ParameterSchemaSpec`、`InferValue`、`InferArgs` |
| [`src/json-schema.ts`](src/json-schema.ts) | 强制执行的原始 JSON Schema 子集与校验 |
| [`src/presentation.ts`](src/presentation.ts) | 带 `card` 标签的 UI 呈现意图 |
-| [`src/code-mode.ts`](src/code-mode.ts) | Code Mode:SDK 生成、`run_code` 分发桥接层、结算 |
+| [`src/ptc.ts`](src/ptc.ts) | PTC mode:SDK 生成、`run_code` 分发桥接层、结算 |
| [`src/ts-types.ts`](src/ts-types.ts) | TypeScript SDK 类型渲染 |
| [`src/py-types.ts`](src/py-types.ts) | Python SDK 类型渲染 |
| [`src/invariant.ts`](src/invariant.ts) | 不变式配套 |
@@ -120,9 +120,9 @@ ctx.tools.register(defineTool({
每次类型化调用都会实体化并冻结解析后的参数、分配不透明关联 token,再运行策略与分发。取消采用协作式并等待完全停稳:每个工具主体都收到调用方拥有的 `exec.signal` 且必须观测它;调用主体前的取消为 `ABORTED_BEFORE_DISPATCH`,调用主体后的取消只能把成功结果替换为 `ABORTED`。拒绝、包装层失败、工具失败、后置策略失败与超时产生的 `TOOL_TIMEOUT` 仍保留更具体的结果。未知工具与抛出异常的工具都会变成结构化错误(`UNKNOWN_TOOL`),因此调用会失败而不会结束轮次。
-### Code Mode
+### PTC mode
-在 `code` 或 `both` 下,注册表公开保留的 `run_code` 传输以及按所加载运行时语言生成的确定性 SDK。每个 SDK 绑定调用都会在日志中与外层调用关联,重新进入完整工具流水线,并通过复用原生并发约定的每次运行独有池调度。在纯 `code` 下,模型直呼其他任何可见工具都会在策略之前解析为 `UNKNOWN_TOOL`——通告面与可调用面保持一致。中间绑定值只存在于执行局部;只有外层 `run_code` 结果有硬大小上限。[执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.zh.md) 拥有该收束约定。
+在 `ptc` 或 `both` 下,注册表公开保留的 `run_code` 传输以及按所加载运行时语言生成的确定性 SDK。每个 SDK 绑定调用都会在日志中与外层调用关联,重新进入完整工具流水线,并通过复用原生并发约定的每次运行独有池调度。在纯 `ptc` 下,模型直呼其他任何可见工具都会在策略之前解析为 `UNKNOWN_TOOL`——通告面与可调用面保持一致。中间绑定值只存在于执行局部;只有外层 `run_code` 结果有硬大小上限。[执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.zh.md) 拥有该收束约定。
### 扩展点
@@ -164,13 +164,13 @@ ctx.tools.register(defineTool({
只要可见定义及其顺序不变,前缀就保持稳定。注册、dispose 或作用域限制可能从第一个改变的 schema token 起使复用失效。
-### Code Mode schema 与系统提示词
+### PTC mode schema 与系统提示词
#### 模型看到什么
-Code Mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tools)、下方 SDK 说明,以及按所加载运行时语言生成的精确 SDK 块。TypeScript 说明会把生成声明明确标为只能在程序内使用的绑定。当当前 `bash` 参数 schema 接受示例参数时,说明还会给出以 `run_code` 包住 `tools.bash(...)` 的完整调用。`tools:sdk` 段使用 first-party 顺序 5000。`both` 会同时公开普通 schema 与此 Code Mode API;在 `code` 下,提示词还会带上处于更早 first-party 顺序的 `tools:code-only` 规则,让模型先读到「可以调用哪些工具」再读「每个工具做什么」。
+PTC mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tools)、下方 SDK 说明,以及按所加载运行时语言生成的精确 SDK 块。TypeScript 说明会把生成声明明确标为只能在程序内使用的绑定。当当前 `bash` 参数 schema 接受示例参数时,说明还会给出以 `run_code` 包住 `tools.bash(...)` 的完整调用。`tools:sdk` 段使用 first-party 顺序 5000。`both` 会同时公开普通 schema 与此 PTC mode API;在 `ptc` 下,提示词还会带上处于更早 first-party 顺序的 `tools:ptc-only` 规则,让模型先读到「可以调用哪些工具」再读「每个工具做什么」。
-##### 带 bash 的 TypeScript Code Mode SDK 说明
+##### 带 bash 的 TypeScript PTC mode SDK 说明
```markdown
## Writing code for run_code
@@ -191,17 +191,17 @@ Program-only SDK bindings:
#### Token 影响
-每次请求的固定成本与可见定义成正比。Code Mode 使用生成的 SDK 文本加一个传输 schema 取代最终工具 schema,但不承诺普遍减少成本。
+每次请求的固定成本与可见定义成正比。PTC mode 使用生成的 SDK 文本加一个传输 schema 取代最终工具 schema,但不承诺普遍减少成本。
#### KV Cache 影响
-只要 Code Mode 选择、生成的 SDK、传输 schema 与可见工具集合不变,前缀就保持稳定。模式或筛选器变更可能从第一个改变的提示词或 schema token 起使复用失效。
+只要 PTC mode 选择、生成的 SDK、传输 schema 与可见工具集合不变,前缀就保持稳定。模式或筛选器变更可能从第一个改变的提示词或 schema token 起使复用失效。
### 工具调用历史与结果
#### 模型看到什么
-循环会保留模型发出的参数与注册表的最终内容。任何抛出异常或遭到拒绝的调用,都会转换为确切的 `Error: `。Code Mode 只返回外层程序打印的行与呈现后的返回值;两者都为空时返回 `(run_code completed with no output)`;失败时返回 `Error: code run failed (): `,并根据是否存在已捕获内容,在其后附加 `Captured output:` 与捕获的行。内部分发事件只保留在日志中;成功且含图片的子结果会在外层结果之后作为带来源归属的上下文追加。
+循环会保留模型发出的参数与注册表的最终内容。任何抛出异常或遭到拒绝的调用,都会转换为确切的 `Error: `。PTC mode 只返回外层程序打印的行与呈现后的返回值;两者都为空时返回 `(run_code completed with no output)`;失败时返回 `Error: code run failed (): `,并根据是否存在已捕获内容,在其后附加 `Captured output:` 与捕获的行。内部分发事件只保留在日志中;成功且含图片的子结果会在外层结果之后作为带来源归属的上下文追加。
#### Token 影响
@@ -222,8 +222,8 @@ Program-only SDK bindings:
- **`tools/pre-execute` 有意不允许改写 `exec.arguments`**:否则日志记录与呈现的参数会与实际运行内容失去同步;改写设计记录在[拟议的 Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.zh.md)中。
- **调用方定义的 subagent 与工作流结构化输出仍要求对象根**:这是消费方层面的守卫;共享 schema 词汇与工具输出支持任意 JSON 根。
- **定义中的 `timeoutMs` 仅作声明之用**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-tool-call-timeout-policy` 包装层。
-- **Code Mode 的 SDK 语言由当前加载的运行时决定,且呈现方式按 agent 而非按工具**:`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language` 有已注册的 SDK 渲染器;同一个 agent 内不能让一个工具仅使用 Native,而另一个仅使用 Code。
-- **Code Mode 中间值只存在于执行局部,且没有字节上限**:它们无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。
+- **PTC mode 的 SDK 语言由当前加载的运行时决定,且呈现方式按 agent 而非按工具**:`mode: ptc`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language` 有已注册的 SDK 渲染器;同一个 agent 内不能让一个工具仅使用 Native,而另一个仅使用 PTC。
+- **PTC mode 中间值只存在于执行局部,且没有字节上限**:它们无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。
- **每次运行都会获得全新的 `run_code` 状态**:MVP 不采用持久 REPL 风格内核,因为跨调用状态不会出现在日志中。
diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json
index aa567e811f..e6888137a6 100644
--- a/packages/core/tools/package.json
+++ b/packages/core/tools/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tools",
"description": "Tool registry and execution pipeline for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -63,6 +63,7 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts
index 8cabf75d10..9513fe0a3c 100644
--- a/packages/core/tools/src/index.ts
+++ b/packages/core/tools/src/index.ts
@@ -21,8 +21,8 @@ import type {} from '@deepseek-ai/dsh-user-approval'
import type { ToolCallView, ToolResultView } from './presentation.ts'
import { assertSupportedJsonSchema, validateJsonSchemaValue } from './json-schema.ts'
import type { JsonSchemaNode } from './json-schema.ts'
-import { createRunCodeTool, RUN_CODE_NAME, SDK_SECTION_ORDER } from './code-mode.ts'
-import type { CodeSdkLanguage } from './code-mode.ts'
+import { createRunCodeTool, RUN_CODE_NAME, SDK_SECTION_ORDER } from './ptc.ts'
+import type { CodeSdkLanguage } from './ptc.ts'
import { renderToolsSdk } from './ts-types.ts'
import type { ToolSdkSchema } from './ts-types.ts'
import { renderToolsSdkPy } from './py-types.ts'
@@ -33,7 +33,7 @@ import { renderToolsSdkPy } from './py-types.ts'
* section under a non-native mode; a runtime whose language is not a key
* fails the assembly loudly (same idiom as `toolOrder` violations). Adding a
* new backend language is three parallel edits — a {@link CodeSdkLanguage}
- * member, an entry here, and a `RUN_CODE_FLAVORS` entry in `code-mode.ts` for
+ * member, an entry here, and a `RUN_CODE_FLAVORS` entry in `ptc.ts` for
* its `run_code` schema strings — plus the renderer function this table points
* at. The `satisfies` clause pins this table's key set to that union, which
* the flavor table is checked against too, so any of the three left out is a
@@ -44,18 +44,18 @@ import { renderToolsSdkPy } from './py-types.ts'
* {@link Config.mode} JSDoc.
*/
/**
- * Prompt order of the `code` collapse statement: after the persona and before
+ * Prompt order of the `ptc` collapse statement: after the persona and before
* per-tool guidance, so the model reads which tools it may call before it
* reads what each one is for.
*/
-const COLLAPSE_SECTION_ORDER = FIRST_PARTY_SECTION_ORDER.CODE_ONLY
+const COLLAPSE_SECTION_ORDER = FIRST_PARTY_SECTION_ORDER.PTC_ONLY
/**
- * The model-facing statement of the `code` collapse. Names the consequence
+ * The model-facing statement of the `ptc` collapse. Names the consequence
* (the call fails) and the route (inside the program), because a rule the
* model can only discover by being denied is one it corrects too late.
*/
-const CODE_ONLY_INSTRUCTION = `\`${RUN_CODE_NAME}\` is the only tool you can call directly — a tool call naming any other tool fails. Reach every tool the SDK declares below from inside the program.`
+const PTC_ONLY_INSTRUCTION = `\`${RUN_CODE_NAME}\` is the only tool you can call directly — a tool call naming any other tool fails. Reach every tool the SDK declares below from inside the program.`
const SDK_RENDERERS: Record string> = {
typescript: renderToolsSdk,
@@ -99,9 +99,9 @@ export {
} from './json-schema.ts'
export type { JsonValue } from '@deepseek-ai/dsh-session'
-export type { CodeDispatchEventData, CodeDispatchStartEventData } from './types.ts'
+export type { PtcDispatchEventData, PtcDispatchStartEventData } from './types.ts'
-export { CodeRunFailedError, RUN_CODE_NAME } from './code-mode.ts'
+export { CodeRunFailedError, RUN_CODE_NAME } from './ptc.ts'
export { jsonSchemaToTs, renderToolsSdk } from './ts-types.ts'
export { jsonSchemaToPy, renderToolsSdkPy } from './py-types.ts'
export { defineContentToolFixture, type ContentToolFixtureOptions } from './testing.ts'
@@ -186,7 +186,7 @@ declare module '@deepseek-ai/cordis' {
* @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
+ 'tools/ptc-dispatch-log'(this: Scoped, dispatch: PtcDispatchLog, next: () => Promise): Promise
/**
* Observe the frozen, lossless-JSON final outcome. Listener failures are contained.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`.
@@ -325,11 +325,11 @@ export interface ToolExecutionInput {
/** The agent on whose behalf the call runs (set by the agent loop). */
readonly agent?: Agent
/**
- * Opaque token of the enclosing transport execution, when one exists. Code
- * Mode sets this on SDK sub-dispatches so commit-style observers can wait for
+ * Opaque token of the enclosing transport execution, when one exists. PTC
+ * mode sets this on SDK sub-dispatches so commit-style observers can wait for
* the outer `run_code` outcome without receiving its live mutable execution.
* The token also marks the call as a transport sub-dispatch rather than a
- * model-direct call: under `mode: 'code'`, only calls WITH a parent may
+ * model-direct call: under `mode: 'ptc'`, only calls WITH a parent may
* execute a native tool name — a model-direct call (no parent) is denied as
* `UNKNOWN_TOOL` before the policy pipeline. See {@link ToolRuntime.execute}.
*/
@@ -348,14 +348,14 @@ export type ToolExecutionMode =
/**
* One settled `run_code` sub-dispatch about to be logged, as seen by the
- * `tools/code-dispatch-log` waterfall: the parent execution (session owner,
+ * `tools/ptc-dispatch-log` waterfall: the parent execution (session owner,
* outer call identity), the sub-call identity, and the outcome whose durable
* copy a listener may reshape. `content` is the RENDERED result projection
* (what a native `tool/result` would carry) — the program itself received
* the structured `value` (or just the error message on failure); only the
* `tool/code-dispatch` event's copy changes.
*/
-export interface CodeDispatchLog {
+export interface PtcDispatchLog {
/** The outer `run_code` execution. */
readonly exec: ToolExecution
/** The calling agent (the scope routing key and the spill owner), when the outer call has one. */
@@ -649,18 +649,18 @@ function errorInfo(error: unknown): ToolErrorInfo | undefined {
}
/** How the registry presents its tools to the model (see {@link Config.mode}). */
-export type ToolPresentationMode = 'native' | 'code' | 'both'
+export type ToolPresentationMode = 'native' | 'ptc' | 'both'
/** Plugin config: how the registered tools are presented to the model. */
export interface Config {
/**
- * Model presentation. `native` (default) sends every visible schema; `code`
+ * Model presentation. `native` (default) sends every visible schema; `ptc`
* sends only `run_code` plus a generated SDK prompt and collapses the
* executor to the same surface (a model-direct call may only name
* `run_code`; `run_code` SDK sub-dispatches keep every visible tool); `both`
- * sends both forms. Code modes require a `ctx.codeRuntime` whose `language`
+ * sends both forms. PTC mode requires a `ctx.codeRuntime` whose `language`
* has a registered SDK renderer (TypeScript or Python) and fail prompt
- * assembly when it is absent or has no renderer. Under `code`, native names
+ * assembly when it is absent or has no renderer. Under `ptc`, native names
* in `toolOrder` are invalid.
*/
mode?: ToolPresentationMode
@@ -676,7 +676,7 @@ export interface Config {
/**
* Per-scope filter over global tools. Restrictions intersect and do not affect
- * scoped registrations or the reserved Code Mode transport.
+ * scoped registrations or the reserved PTC mode transport.
*/
export interface ToolRestriction {
/** Global tool names that stay visible; everything else is removed. */
@@ -789,7 +789,7 @@ export class ToolRuntime extends Service {
static inject = ['systemPrompt']
static Config: z = z.object({
- mode: z.union(['native', 'code', 'both'] as const).default('native'),
+ mode: z.union(['native', 'ptc', 'both'] as const).default('native'),
maxParallelSubCalls: z.natural().min(1).default(10),
})
@@ -819,10 +819,10 @@ export class ToolRuntime extends Service {
/**
* Reserved presentation transport, kept outside the filterable registration
* layers. Built on first need rather than at construction: which agents run
- * a code mode is no longer known when the service is constructed, and the
+ * a PTC mode is no longer known when the service is constructed, and the
* transport is stateless beyond its closures over `this`.
*/
- private codeTransport: ToolDefinition | undefined
+ private ptcTransport: ToolDefinition | undefined
constructor(ctx: Context, config: Config = {}) {
super(ctx, 'tools')
@@ -838,8 +838,8 @@ export class ToolRuntime extends Service {
}
/**
- * The prompt statement of the `code` executor collapse, registered wherever
- * {@link sdkSection} is and rendering empty outside an effective `code`.
+ * The prompt statement of the `ptc` executor collapse, registered wherever
+ * {@link sdkSection} is and rendering empty outside an effective `ptc`.
*
* Every tool contributes its own guidance section naming its tool, none of
* them qualify how that tool is reached, and they all render before the SDK.
@@ -854,20 +854,20 @@ export class ToolRuntime extends Service {
*/
private collapseSection(): { name: string; order: number; text: (context: { scope?: ScopeKey }) => string } {
return {
- name: 'tools:code-only',
+ name: 'tools:ptc-only',
order: COLLAPSE_SECTION_ORDER,
// The SAME predicate the executor denies by, so the prompt cannot state
// a rule the registry does not enforce (see `collapses`).
- text: context => this.modeFor(context.scope) === 'code' ? CODE_ONLY_INSTRUCTION : '',
+ text: context => this.modeFor(context.scope) === 'ptc' ? PTC_ONLY_INSTRUCTION : '',
}
}
/**
- * The generated-SDK prompt section, registered globally by a code-mode
+ * The generated-SDK prompt section, registered globally by a PTC mode
* deployment and per scope by {@link presentAs}.
*
* The body regenerates from the CALLING scope, and renders empty for an
- * agent presenting natively — an agent that opted out under a code-mode
+ * agent presenting natively — an agent that opted out under a PTC mode
* deployment still sees the global registration, and an empty section is
* dropped from the rendered prompt.
* @returns the section registration.
@@ -920,7 +920,7 @@ export class ToolRuntime extends Service {
* @returns the shared transport definition.
*/
private requireCodeTransport(): ToolDefinition {
- this.codeTransport ??= createRunCodeTool(this, {
+ this.ptcTransport ??= createRunCodeTool(this, {
requireRuntime: () => this.requireCodeRuntime(this.defaultMode),
// The language-aware description/parameters getters read the runtime
// without demanding one, so a native-default process can still project
@@ -929,7 +929,7 @@ export class ToolRuntime extends Service {
maxParallel: this.maxParallelSubCalls,
shapeDispatchLog: dispatch => this.shapeDispatchLog(dispatch),
})
- return this.codeTransport
+ return this.ptcTransport
}
/**
@@ -938,7 +938,7 @@ export class ToolRuntime extends Service {
* declaration covers every agent joined under it.
*
* Scoped only, and one declaration per scope: this is how an agent preset
- * composes Code Mode agents beside native ones in the same process, and a
+ * composes PTC mode agents beside native ones in the same process, and a
* process-global override would be the `mode` config field instead.
* @param mode - the presentation the covered agents' models see.
* @returns the exact disposer that restores the deployment default.
@@ -961,7 +961,7 @@ export class ToolRuntime extends Service {
{ label: 'tools.presentAs()' },
)
// The SDK and collapse sections are per scope for the same reason the
- // mode is. Under a deployment that already defaults to a code mode this
+ // mode is. Under a deployment that already defaults to PTC mode this
// shadows the global registration with an identical body, which costs
// nothing and keeps one rule instead of a case analysis.
if (mode !== 'native') {
@@ -991,7 +991,7 @@ export class ToolRuntime extends Service {
// language with no SDK renderer.
this.requireCodeRuntime(mode)
const schemas = [...view.visible.values()].map(definition => this.schemaOf(definition, false))
- if (mode === 'code') {
+ if (mode === 'ptc') {
return {
schemas: schemas.filter(schema => schema.name === RUN_CODE_NAME),
knownNames: [RUN_CODE_NAME],
@@ -1013,7 +1013,7 @@ export class ToolRuntime extends Service {
* language between them would hand a program written against one SDK to the
* other. Binding it is deferred until a second backend ships (the first
* point it is testable); rationale in the
- * [language-dispatch note](../../../../.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md).
+ * [language-dispatch note](../../../../.agents/notes/implemented/feature/2026-07-31-ptc-language-dispatch.md).
*/
private requireCodeRuntime(mode: ToolPresentationMode): CodeRuntime {
const runtime = this.ctx.get('codeRuntime')
@@ -1051,7 +1051,7 @@ export class ToolRuntime extends Service {
// so a name free to take under the deployment default would become a
// collision the moment a preset mounted.
if (name === RUN_CODE_NAME) {
- throw new Error(`tool name "${RUN_CODE_NAME}" is reserved for the Code Mode presentation transport and cannot be registered or shadowed`)
+ throw new Error(`tool name "${RUN_CODE_NAME}" is reserved for the PTC mode presentation transport and cannot be registered or shadowed`)
}
return this.layers.effect(
this.ctx,
@@ -1082,7 +1082,7 @@ export class ToolRuntime extends Service {
...deny !== undefined ? { deny: new Set(deny) } : {},
}
if ([...allow ?? [], ...deny ?? []].includes(RUN_CODE_NAME)) {
- throw new Error(`tools.restrict() cannot name reserved Code Mode presentation transport "${RUN_CODE_NAME}"; restrict end-capability tools instead`)
+ throw new Error(`tools.restrict() cannot name reserved PTC mode presentation transport "${RUN_CODE_NAME}"; restrict end-capability tools instead`)
}
const known = this.view(scope).restrictableNames
const unknown = [...allow ?? [], ...deny ?? []].filter(name => !known.has(name))
@@ -1207,7 +1207,7 @@ export class ToolRuntime extends Service {
/**
* Resolve the definition that MAY EXECUTE for a call, applying the mode
* collapse at the operation boundary that owns it. The registry view
- * (`get`) is presentation-agnostic; here a MODEL-DIRECT call under `code`
+ * (`get`) is presentation-agnostic; here a MODEL-DIRECT call under `ptc`
* may only name the reserved `run_code` transport, while a nested
* sub-dispatch (a `parent` token set — the `run_code` SDK calling a tool
* it bound) may call any visible tool. Denial surfaces as `UNKNOWN_TOOL`
@@ -1234,7 +1234,7 @@ export class ToolRuntime extends Service {
return [...this.view(scope).visible.values()].map(definition => this.schemaOf(definition, true))
}
- /** Project visible callable tools onto the generated Code Mode SDK contract. */
+ /** Project visible callable tools onto the generated PTC mode SDK contract. */
private sdkSchemas(scope?: ScopeKey): ToolSdkSchema[] {
return [...this.view(scope).visible.values()]
.filter(definition => definition.name !== RUN_CODE_NAME)
@@ -1284,7 +1284,7 @@ export class ToolRuntime extends Service {
}
/**
- * Run the `tools/code-dispatch-log` waterfall over one settled sub-dispatch
+ * Run the `tools/ptc-dispatch-log` waterfall over one settled sub-dispatch
* and return the content the bridge should log on `tool/code-dispatch`.
* Contained: when a listener throws, the method logs the original settled
* content; that failure must not fail the dispatch or omit the settle event. Private:
@@ -1292,26 +1292,26 @@ export class ToolRuntime extends Service {
* receives it as a capability parameter (the `requireRuntime` idiom) — the
* waterfall, not this invoker, is the public extension point.
*/
- private async shapeDispatchLog(dispatch: CodeDispatchLog): Promise {
+ private async shapeDispatchLog(dispatch: PtcDispatchLog): Promise {
try {
return await this.ctx.waterfall(
- scopeTarget(this, dispatch.agent), 'tools/code-dispatch-log', dispatch,
+ scopeTarget(this, dispatch.agent), 'tools/ptc-dispatch-log', dispatch,
() => Promise.resolve(dispatch.content),
)
} catch (error: unknown) {
- this.ctx.logger.warn(`tools: code-dispatch-log listener failed for ${dispatch.name}: ${errorMessage(error)}; logging the original settled content`)
+ this.ctx.logger.warn(`tools: ptc-dispatch-log listener failed for ${dispatch.name}: ${errorMessage(error)}; logging the original settled content`)
return dispatch.content
}
}
/**
- * Whether the `code` mode collapse denies a model-direct call: only the
+ * Whether the `ptc` mode collapse denies a model-direct call: only the
* reserved `run_code` transport may be named. Nested sub-dispatches (a
* `parent` token set) bypass the collapse. One home for the
* security-relevant predicate, shared by {@link resolveExecution} and
* {@link createExecution} so the two can never drift apart.
*
- * Resolved through {@link modeFor}, NOT `defaultMode`: an agent given `code`
+ * Resolved through {@link modeFor}, NOT `defaultMode`: an agent given `ptc`
* by an agent preset under a native deployment is the composition
* `dsh-agent-tool-presentation` exists for, and reading the deployment default would
* leave exactly that agent uncollapsed — announcing one surface while
@@ -1321,7 +1321,7 @@ export class ToolRuntime extends Service {
* @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
*/
private collapses(name: string, scope: ScopeKey | undefined, nested: boolean): boolean {
- return !nested && this.modeFor(scope) === 'code' && name !== RUN_CODE_NAME
+ return !nested && this.modeFor(scope) === 'ptc' && name !== RUN_CODE_NAME
}
/**
@@ -1370,7 +1370,7 @@ export class ToolRuntime extends Service {
const parent = exec.parent
const signal = exec.signal
// Distinguish a mode-collapsed call (visible in the scope, denied only by
- // the `code` collapse) from a genuinely unknown tool. A collapsed call is
+ // the `ptc` collapse) from a genuinely unknown tool. A collapsed call is
// deterministically denied, so it terminates BEFORE the extensible policy
// pipeline: pre-execute listeners, approval `ask`, and guards must never
// observe — or worse, approve — a call that can only fail. An unknown tool
diff --git a/packages/core/tools/src/json-schema.ts b/packages/core/tools/src/json-schema.ts
index 9191bcfbfa..701652ceb8 100644
--- a/packages/core/tools/src/json-schema.ts
+++ b/packages/core/tools/src/json-schema.ts
@@ -1,5 +1,5 @@
/**
- * Enforced JSON Schema subset shared by tool outputs, generated Code Mode
+ * Enforced JSON Schema subset shared by tool outputs, generated PTC mode
* types, subagents, and workflows. The subset accepts any JSON root, an
* annotation-only schema for unconstrained JSON, one scalar `type`, object
* `properties`/`required`/boolean `additionalProperties`, array `items`,
diff --git a/packages/core/tools/src/code-mode.ts b/packages/core/tools/src/ptc.ts
similarity index 98%
rename from packages/core/tools/src/code-mode.ts
rename to packages/core/tools/src/ptc.ts
index 0d97797154..bd60b9f97b 100644
--- a/packages/core/tools/src/code-mode.ts
+++ b/packages/core/tools/src/ptc.ts
@@ -1,9 +1,9 @@
/**
- * Code Mode `run_code` transport. Programs call the registry's agent-visible
+ * PTC mode `run_code` transport. Programs call the registry's agent-visible
* tools through nested executions scheduled under the native concurrency
* contract; each sub-dispatch is logged for reconstruction, while only the
* outer curated result enters model history.
- * @module @deepseek-ai/dsh-tools/src/code-mode
+ * @module @deepseek-ai/dsh-tools/src/ptc
*/
import { ToolCallId, createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm'
@@ -14,10 +14,10 @@ import type { JsonValue } from '@deepseek-ai/dsh-session'
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
import { defineTool, parameterSchemaSpecToJsonSchema } from './schema.ts'
import { TOOL_RUNTIME_SCHEDULER } from './index.ts'
-import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRuntime, ToolRunContext } from './index.ts'
+import type { PtcDispatchLog, ToolDefinition, ToolExecutionResult, ToolRuntime, ToolRunContext } from './index.ts'
import type {} from './types.ts'
-/** The model-facing name of the Code Mode tool. */
+/** The model-facing name of the PTC mode tool. */
export const RUN_CODE_NAME = 'run_code'
/** The `tools:sdk` section order, after per-tool guidance sections. */
@@ -72,7 +72,7 @@ const PYTHON_FLAVOR: RunCodeFlavor = {
}
/**
- * The languages Code Mode ships a presentation for. Both per-language tables —
+ * The languages PTC mode ships a presentation for. Both per-language tables —
* {@link RUN_CODE_FLAVORS} here and `SDK_RENDERERS` in {@link ./index.ts} — are
* checked against this union with `satisfies`, so a language added to one and
* not the other fails `typecheck` instead of waiting for a runtime that reports
@@ -259,7 +259,7 @@ function renderValue(value: JsonValue): string {
return typeof value === 'string' ? value : renderJsonValue(value)
}
-/** Canonical value returned by the outer Code Mode transport. */
+/** Canonical value returned by the outer PTC mode transport. */
type RunCodeOutput = { logs: string[]; result?: JsonValue }
/**
@@ -279,8 +279,8 @@ export interface RunCodeBridgeOptions {
peekRuntime: () => CodeRuntime | undefined
/** The run's overlap cap for parallel-classified sub-calls (the registry passes its validated `maxParallelSubCalls`). */
maxParallel: number
- /** Runs the contained `tools/code-dispatch-log` waterfall over one settled sub-dispatch (the registry's private invoker). */
- shapeDispatchLog: (dispatch: CodeDispatchLog) => Promise
+ /** Runs the contained `tools/ptc-dispatch-log` waterfall over one settled sub-dispatch (the registry's private invoker). */
+ shapeDispatchLog: (dispatch: PtcDispatchLog) => Promise
}
/**
diff --git a/packages/core/tools/src/py-types.ts b/packages/core/tools/src/py-types.ts
index bb03ed64ee..74669a9739 100644
--- a/packages/core/tools/src/py-types.ts
+++ b/packages/core/tools/src/py-types.ts
@@ -1,11 +1,11 @@
/**
- * Code Mode codegen — Python flavor. The pure projection from registered tool schemas to the
+ * PTC mode codegen — Python flavor. The pure projection from registered tool schemas to the
* Python SDK text the model programs against under `runtime.language === 'python'`. Sibling of
* {@link ./ts-types.ts | ts-types.ts}; the two files are two projections of the same registry
* store, keyed by the loaded {@link @deepseek-ai/dsh-code-runtime#CodeRuntime.language | code
* runtime's language}.
*
- * Under `mode: 'code'` the native tool schemas are omitted from the request, so this generated
+ * Under `mode: 'ptc'` the native tool schemas are omitted from the request, so this generated
* SDK is the model's ONLY source for each tool's argument names, required fields, types,
* descriptions, and canonical output shapes; under `mode: 'both'` the native schemas ship
* alongside it and it is one of two. Object-shaped arguments and outputs therefore render as one
@@ -32,7 +32,7 @@ const IDENTIFIER = /^[\p{XID_Start}_]\p{XID_Continue}*$/u
* Python identifiers are not ASCII: `路径` is as legal a field name as `path`,
* and rejecting it would degrade the whole enclosing object, dropping every
* field's name, requiredness, and type — information whose only source under
- * `mode: 'code'` is this generated text.
+ * `mode: 'ptc'` is this generated text.
*
* NFKC stability is a second and separate condition, because CPython
* normalizes identifiers at compile time while JSON keys are compared as
@@ -163,7 +163,7 @@ interface RenderState {
* (`SyntaxError: source code string cannot contain null bytes`), whether it
* sits in a docstring or in a comment, so one such byte anywhere in a schema
* description would make the whole generated SDK unparseable — under
- * `mode: 'code'`, the model's only declaration of the tools. The rest are
+ * `mode: 'ptc'`, the model's only declaration of the tools. The rest are
* legal but invisible; escaping them with the same rule keeps the emitted text
* readable and the treatment uniform.
*
@@ -236,7 +236,7 @@ function describe(schema: object): string | undefined {
* Backslashes are doubled first, every quote is escaped, and a trailing
* backslash cannot survive: a description ending in `"` or an odd backslash
* would otherwise merge with (or escape) the closing triple quote and make
- * the generated block — Code Mode's only SDK — syntactically invalid Python.
+ * the generated block — PTC mode's only SDK — syntactically invalid Python.
*/
function docLines(description: unknown, indent: number): string[] {
const collapsed = describe({ description })
@@ -579,7 +579,7 @@ function renderType(schema: unknown, className: string, state: RenderState): str
}
}
// TypedDict syntax cannot express openness, so an open object states it
- // in-band: the annotation is advisory either way, and `mode: 'code'`
+ // in-band: the annotation is advisory either way, and `mode: 'ptc'`
// omits the native schemas, making this line the model's only signal
// that extra keys are accepted.
if (node.additionalProperties !== false) {
@@ -778,7 +778,7 @@ export function renderToolsSdkPy(schemas: ToolSdkSchema[]): string {
// of that method's body. Emitted before the `async def` it would instead
// become the `Tools` class docstring (for the first tool) or a dead
// expression (for every later one), leaving every method undocumented —
- // and under `mode: 'code'` this SDK is the model's only description of
+ // and under `mode: 'ptc'` this SDK is the model's only description of
// what a tool does. A docstring is a complete body, so the `...` stub is
// only for the description-less case.
const doc = docLines(schema.description, 2)
diff --git a/packages/core/tools/src/ts-types.ts b/packages/core/tools/src/ts-types.ts
index a5d36a5ce3..d92c987817 100644
--- a/packages/core/tools/src/ts-types.ts
+++ b/packages/core/tools/src/ts-types.ts
@@ -1,5 +1,5 @@
/**
- * Code Mode codegen: the pure projection from registered tool schemas to the TypeScript SDK
+ * PTC mode codegen: the pure projection from registered tool schemas to the TypeScript SDK
* text the model programs against (the `tools:sdk` prompt section). Sibling of
* `json-schema.ts` — `schemas()` (native function calling) and this module (the generated
* `declare const tools` API) are two projections of the same store.
@@ -9,7 +9,7 @@
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
import { assertSupportedJsonSchema } from './json-schema.ts'
import type { JsonSchemaNode, JsonSchemaScalar } from './json-schema.ts'
-/** Internal Code Mode projection: the model-facing schema plus the canonical output schema. */
+/** Internal PTC mode projection: the model-facing schema plus the canonical output schema. */
export interface ToolSdkSchema extends ToolSchema {
/** Validated canonical value returned by the tool binding. */
output: JsonSchemaNode
@@ -246,7 +246,7 @@ export function jsonSchemaToTs(schema: unknown, indent = 0): string {
}
}
-/** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */
+/** The fixed model-facing usage contract rendered above the declarations (see the PTC mode Agent Note's "What the model sees"). */
const SDK_INSTRUCTIONS = `## Writing code for run_code
\`run_code\` takes two required arguments: \`code\` — the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped) — and \`description\`, a short summary of what the program does. The declarations below are SDK bindings for this program. A declaration does not make its name a directly callable tool; only names supplied as separate tool schemas may be called directly.`
diff --git a/packages/core/tools/src/types.ts b/packages/core/tools/src/types.ts
index 6de9c10714..84052261ab 100644
--- a/packages/core/tools/src/types.ts
+++ b/packages/core/tools/src/types.ts
@@ -7,8 +7,8 @@
import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
-/** Payload recorded when one nested Code Mode Tool dispatch starts. */
-export interface CodeDispatchStartEventData {
+/** Payload recorded when one nested PTC mode Tool dispatch starts. */
+export interface PtcDispatchStartEventData {
rootCallId: ToolCallId
parentCallId: ToolCallId
subCallId: ToolCallId
@@ -16,8 +16,8 @@ export interface CodeDispatchStartEventData {
arguments: unknown
}
-/** Payload recorded when one nested Code Mode Tool dispatch settles. */
-export interface CodeDispatchEventData extends CodeDispatchStartEventData {
+/** Payload recorded when one nested PTC mode Tool dispatch settles. */
+export interface PtcDispatchEventData extends PtcDispatchStartEventData {
isError: boolean
content: ContentBlock[]
}
@@ -37,7 +37,7 @@ declare module '@deepseek-ai/dsh-session/types' {
* with `tool/code-dispatch` by `subCallId` (timing = the two events'
* `time` fields).
*/
- 'tool/code-dispatch-start': CodeDispatchStartEventData
+ 'tool/code-dispatch-start': PtcDispatchStartEventData
/**
* One bridged sub-dispatch SETTLING: the pairing ids (matching the
* `tool/code-dispatch-start` with the same `subCallId`), the tool `name`
@@ -53,6 +53,6 @@ declare module '@deepseek-ai/dsh-session/types' {
* before returning), so its execution-enclosure relation holds by
* construction.
*/
- 'tool/code-dispatch': CodeDispatchEventData
+ 'tool/code-dispatch': PtcDispatchEventData
}
}
diff --git a/packages/core/tools/tests/invariant.spec.ts b/packages/core/tools/tests/invariant.spec.ts
index 24c3c443f4..db3c0b905c 100644
--- a/packages/core/tools/tests/invariant.spec.ts
+++ b/packages/core/tools/tests/invariant.spec.ts
@@ -89,7 +89,7 @@ describe('tool-pipeline invariants', () => {
expect(() => { emitResult(ctx, anonymous, outcome()) }).toThrow(/non-empty name and callId/)
})
- it('requires code-dispatch records to be turn-enclosed', async () => {
+ it('requires ptc-dispatch records to be turn-enclosed', async () => {
const ctx = await setup()
const session = ctx.sessions.create()
const data = {
@@ -204,7 +204,7 @@ describe('tool-pipeline invariants', () => {
}).not.toThrow()
})
- it('replays enclosed code-dispatch records on late registration', async () => {
+ it('replays enclosed ptc-dispatch records on late registration', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const session = ctx.sessions.create()
@@ -223,7 +223,7 @@ describe('tool-pipeline invariants', () => {
await expect(ctx.plugin(ToolsInvariant).then(() => undefined)).resolves.toBeUndefined()
})
- it('rejects an unenclosed code-dispatch record on late registration', async () => {
+ it('rejects an unenclosed ptc-dispatch record on late registration', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
ctx.sessions.create().append('tool/code-dispatch-start', {
diff --git a/packages/core/tools/tests/code-mode.spec.ts b/packages/core/tools/tests/ptc.spec.ts
similarity index 91%
rename from packages/core/tools/tests/code-mode.spec.ts
rename to packages/core/tools/tests/ptc.spec.ts
index 6bad231737..6a29701aa4 100644
--- a/packages/core/tools/tests/code-mode.spec.ts
+++ b/packages/core/tools/tests/ptc.spec.ts
@@ -15,7 +15,7 @@ import type { JsonValue, SessionEventMap } from '@deepseek-ai/dsh-session'
const testToolSignal = new AbortController().signal
/**
- * Code Mode unit tier (per the Agent Note's plan): provider contribution per mode,
+ * PTC mode unit tier (per the Agent Note's plan): provider contribution per mode,
* misconfiguration rejections, the run_code dispatch bridge (serialization,
* abort, JSON normalization, error mapping, events, quiescence), and HMR
* safety — all against an in-repo fake runtime, exactly the
@@ -50,7 +50,7 @@ interface SetupOptions {
async function setup(options: SetupOptions = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt, { ...options.toolOrder ? { toolOrder: options.toolOrder } : {} })
- await ctx.plugin(ToolRuntime, { mode: options.mode ?? 'code', ...options.maxParallelSubCalls !== undefined ? { maxParallelSubCalls: options.maxParallelSubCalls } : {} })
+ await ctx.plugin(ToolRuntime, { mode: options.mode ?? 'ptc', ...options.maxParallelSubCalls !== undefined ? { maxParallelSubCalls: options.maxParallelSubCalls } : {} })
let runtime: FakeRuntime | undefined
if (options.runtime !== false) {
await ctx.plugin(FakeRuntime, options.runtime ?? {})
@@ -124,8 +124,8 @@ describe('mode-aware wire contribution', () => {
expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
})
- it("mode 'code' contributes exactly [run_code] plus the SDK section declaring the other tools", async () => {
- const { ctx, systemPrompt } = await setup({ mode: 'code' })
+ it("mode 'ptc' contributes exactly [run_code] plus the SDK section declaring the other tools", async () => {
+ const { ctx, systemPrompt } = await setup({ mode: 'ptc' })
registerEcho(ctx)
const assembly = await systemPrompt.assemble()
expect(assembly.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
@@ -136,8 +136,8 @@ describe('mode-aware wire contribution', () => {
expect(sdk?.text).not.toContain('tools.bash(')
})
- it("mode 'code' states the run_code-only rule BEFORE the per-tool guidance that names each tool", async () => {
- const { ctx, systemPrompt } = await setup({ mode: 'code' })
+ it("mode 'ptc' states the run_code-only rule BEFORE the per-tool guidance that names each tool", async () => {
+ const { ctx, systemPrompt } = await setup({ mode: 'ptc' })
registerEcho(ctx)
// Stand in for a real tool's guidance section, which names its tool without
// saying how it is reached.
@@ -149,11 +149,11 @@ describe('mode-aware wire contribution', () => {
const assembly = await systemPrompt.assemble()
const names = assembly.sections.map(section => section.name)
- const rule = assembly.sections.find(section => section.name === 'tools:code-only')
+ const rule = assembly.sections.find(section => section.name === 'tools:ptc-only')
expect(rule?.text).toContain(`\`${RUN_CODE_NAME}\` is the only tool you can call directly`)
// The rule is worthless after the guidance it qualifies.
- expect(names.indexOf('tools:code-only')).toBeLessThan(names.indexOf('tool:echo'))
- expect(names.indexOf('tools:code-only')).toBeLessThan(names.indexOf('tools:sdk'))
+ expect(names.indexOf('tools:ptc-only')).toBeLessThan(names.indexOf('tool:echo'))
+ expect(names.indexOf('tools:ptc-only')).toBeLessThan(names.indexOf('tools:sdk'))
})
it("mode 'both' omits the run_code-only rule, because native calls do execute there", async () => {
@@ -162,12 +162,12 @@ describe('mode-aware wire contribution', () => {
const assembly = await systemPrompt.assemble()
// Registered (the deployment is non-native) but empty, so the renderer
// drops it: `both` executes the native call the rule would forbid.
- expect(assembly.sections.find(section => section.name === 'tools:code-only')?.text).toBe('')
+ expect(assembly.sections.find(section => section.name === 'tools:ptc-only')?.text).toBe('')
expect(assembly.tools.map(tool => tool.name)).toContain('echo')
})
- it('projects deeply nested output schemas into the Code Mode SDK without structured-clone recursion', async () => {
- const { ctx, systemPrompt } = await setup({ mode: 'code' })
+ it('projects deeply nested output schemas into the PTC mode SDK without structured-clone recursion', async () => {
+ const { ctx, systemPrompt } = await setup({ mode: 'ptc' })
let output: JsonSchemaNode = { type: 'string' }
for (let depth = 0; depth < 5_000; depth++) {
output = { oneOf: [output, { type: 'null' }] }
@@ -190,7 +190,7 @@ describe('mode-aware wire contribution', () => {
expect(sdk).toContain('deep_output: string | null')
})
- it.each(['code', 'both'] as const)('treats expert assembly output as authoritative in mode %s', async (mode) => {
+ it.each(['ptc', 'both'] as const)('treats expert assembly output as authoritative in mode %s', async (mode) => {
const { ctx, systemPrompt } = await setup({ mode })
registerEcho(ctx)
ctx.on('system-prompt/assemble', async (_assembly, _context, next) => {
@@ -207,7 +207,7 @@ describe('mode-aware wire contribution', () => {
expect(assembly.tools.some(tool => tool.name === RUN_CODE_NAME)).toBe(false)
})
- it.each(['code', 'both'] as const)('lets one scope shadow the default SDK section in mode %s', async (mode) => {
+ it.each(['ptc', 'both'] as const)('lets one scope shadow the default SDK section in mode %s', async (mode) => {
const { ctx, systemPrompt } = await setup({ mode })
registerEcho(ctx)
const { scope, agent } = await mintAgentScope(ctx)
@@ -231,7 +231,7 @@ describe('mode-aware wire contribution', () => {
expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(true)
})
- it.each(['code', 'both'] as const)('keeps the run_code transport outside scoped allow-list filtering in mode %s', async (mode) => {
+ it.each(['ptc', 'both'] as const)('keeps the run_code transport outside scoped allow-list filtering in mode %s', async (mode) => {
const { ctx, systemPrompt, runtime } = await setup({ mode })
registerEcho(ctx, 'echo')
registerEcho(ctx, 'hidden')
@@ -239,7 +239,7 @@ describe('mode-aware wire contribution', () => {
const lift = scope.ctx.tools.restrict({ allow: ['echo'] })
const assembly = await systemPrompt.assemble({ scope: agent })
- expect(assembly.tools.map(tool => tool.name)).toEqual(mode === 'code'
+ expect(assembly.tools.map(tool => tool.name)).toEqual(mode === 'ptc'
? [RUN_CODE_NAME]
: ['echo', RUN_CODE_NAME])
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text
@@ -256,12 +256,12 @@ describe('mode-aware wire contribution', () => {
lift()
const unrestricted = await systemPrompt.assemble({ scope: agent })
- expect(unrestricted.tools.map(tool => tool.name)).toEqual(mode === 'code'
+ expect(unrestricted.tools.map(tool => tool.name)).toEqual(mode === 'ptc'
? [RUN_CODE_NAME]
: ['echo', 'hidden', RUN_CODE_NAME])
})
- it.each(['code', 'both'] as const)('keeps the run_code transport outside scoped deny-list filtering in mode %s', async (mode) => {
+ it.each(['ptc', 'both'] as const)('keeps the run_code transport outside scoped deny-list filtering in mode %s', async (mode) => {
const { ctx, systemPrompt, runtime } = await setup({ mode })
registerEcho(ctx, 'denied')
registerEcho(ctx, 'kept')
@@ -269,7 +269,7 @@ describe('mode-aware wire contribution', () => {
scope.ctx.tools.restrict({ deny: ['denied'] })
const assembly = await systemPrompt.assemble({ scope: agent })
- expect(assembly.tools.map(tool => tool.name)).toEqual(mode === 'code'
+ expect(assembly.tools.map(tool => tool.name)).toEqual(mode === 'ptc'
? [RUN_CODE_NAME]
: ['kept', RUN_CODE_NAME])
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text
@@ -285,7 +285,7 @@ describe('mode-aware wire contribution', () => {
expect(result.content).toEqual([{ type: 'text', text: 'kept' }])
})
- it.each(['code', 'both'] as const)('reserves run_code against scoped shadows and explicit restrictions in mode %s', async (mode) => {
+ it.each(['ptc', 'both'] as const)('reserves run_code against scoped shadows and explicit restrictions in mode %s', async (mode) => {
const { ctx, systemPrompt } = await setup({ mode })
const { scope, agent } = await mintAgentScope(ctx)
const impostor = defineContentToolFixture({
@@ -295,10 +295,10 @@ describe('mode-aware wire contribution', () => {
execute: () => Promise.resolve([{ type: 'text' as const, text: 'impostor' }]),
})
- expect(() => scope.ctx.tools.register(impostor)).toThrow(/reserved for the Code Mode presentation transport/)
- expect(() => ctx.tools.register(impostor)).toThrow(/reserved for the Code Mode presentation transport/)
- expect(() => scope.ctx.tools.restrict({ allow: [RUN_CODE_NAME] })).toThrow(/cannot name reserved Code Mode presentation transport/)
- expect(() => scope.ctx.tools.restrict({ deny: [RUN_CODE_NAME] })).toThrow(/cannot name reserved Code Mode presentation transport/)
+ expect(() => scope.ctx.tools.register(impostor)).toThrow(/reserved for the PTC mode presentation transport/)
+ expect(() => ctx.tools.register(impostor)).toThrow(/reserved for the PTC mode presentation transport/)
+ expect(() => scope.ctx.tools.restrict({ allow: [RUN_CODE_NAME] })).toThrow(/cannot name reserved PTC mode presentation transport/)
+ expect(() => scope.ctx.tools.restrict({ deny: [RUN_CODE_NAME] })).toThrow(/cannot name reserved PTC mode presentation transport/)
scope.ctx.systemPrompt.section({
name: 'scoped-note',
order: FIRST_PARTY_SECTION_ORDER.TOOLS_SDK - 10,
@@ -322,7 +322,7 @@ describe('mode-aware wire contribution', () => {
expect(result.content).toEqual([{ type: 'text', text: '(run_code completed with no output)' }])
})
- it.each(['code', 'both'] as const)('keeps run_code in the toolOrder universe without exposing it as a restriction target in mode %s', async (mode) => {
+ it.each(['ptc', 'both'] as const)('keeps run_code in the toolOrder universe without exposing it as a restriction target in mode %s', async (mode) => {
const { ctx, systemPrompt } = await setup({
mode,
toolOrder: [RUN_CODE_NAME, ''],
@@ -331,7 +331,7 @@ describe('mode-aware wire contribution', () => {
const { agent } = await mintAgentScope(ctx)
const assembly = await systemPrompt.assemble({ scope: agent })
- expect(assembly.tools.map(tool => tool.name)).toEqual(mode === 'code'
+ expect(assembly.tools.map(tool => tool.name)).toEqual(mode === 'ptc'
? [RUN_CODE_NAME]
: [RUN_CODE_NAME, 'echo'])
})
@@ -361,7 +361,7 @@ describe('mode-aware wire contribution', () => {
})
it('renders byte-identical SDK text across consecutive assemblies of an unchanged tool set', async () => {
- const { ctx, systemPrompt } = await setup({ mode: 'code' })
+ const { ctx, systemPrompt } = await setup({ mode: 'ptc' })
registerEcho(ctx)
const first = await systemPrompt.assemble()
const second = await systemPrompt.assemble()
@@ -370,17 +370,17 @@ describe('mode-aware wire contribution', () => {
})
it('rejects every assembly when a non-native mode has no code runtime', async () => {
- const { systemPrompt } = await setup({ mode: 'code', runtime: false })
+ const { systemPrompt } = await setup({ mode: 'ptc', runtime: false })
await expect(systemPrompt.assemble()).rejects.toThrow(/requires a code runtime/)
})
it('rejects every assembly when the runtime language has no registered SDK renderer', async () => {
- const { systemPrompt } = await setup({ mode: 'code', runtime: { language: 'ruby' } })
+ const { systemPrompt } = await setup({ mode: 'ptc', runtime: { language: 'ruby' } })
await expect(systemPrompt.assemble()).rejects.toThrow(/no SDK renderer registered for runtime language "ruby"/)
})
it('assembles under a python runtime by picking the Python SDK renderer', async () => {
- const { ctx, systemPrompt } = await setup({ mode: 'code', runtime: { language: 'python' } })
+ const { ctx, systemPrompt } = await setup({ mode: 'ptc', runtime: { language: 'python' } })
registerEcho(ctx)
const assembly = await systemPrompt.assemble()
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')
@@ -391,7 +391,7 @@ describe('mode-aware wire contribution', () => {
it("assembles under a python runtime in mode 'both' as well, SDK and schema together", async () => {
// `both` reaches the same wireSchemas/requireCodeRuntime/SDK-section code
- // as `code`, so this pins the mode-by-language matrix rather than a
+ // as `ptc`, so this pins the mode-by-language matrix rather than a
// separate path — including that the `wireSchemas` projection behind
// `assembly.tools` picks the Python flavor under `both` instead of hitting
// the flavor-table guard.
@@ -401,12 +401,12 @@ describe('mode-aware wire contribution', () => {
expect(assembly.sections.find(section => section.name === 'tools:sdk')?.text).toContain('class Tools(Protocol):')
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
expect(runCodeSchema?.description).toContain('Execute a Python program')
- // `both` keeps the native tools alongside run_code; `code` does not.
+ // `both` keeps the native tools alongside run_code; `ptc` does not.
expect(assembly.tools.map(tool => tool.name)).toContain('echo')
})
it('emits a TypeScript-flavored run_code schema under a typescript runtime', async () => {
- const { ctx, systemPrompt } = await setup({ mode: 'code', runtime: { language: 'typescript' } })
+ const { ctx, systemPrompt } = await setup({ mode: 'ptc', runtime: { language: 'typescript' } })
registerEcho(ctx)
const assembly = await systemPrompt.assemble()
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
@@ -421,7 +421,7 @@ describe('mode-aware wire contribution', () => {
})
it('emits a Python-flavored run_code schema under a python runtime (matches the SDK language)', async () => {
- const { ctx, systemPrompt } = await setup({ mode: 'code', runtime: { language: 'python' } })
+ const { ctx, systemPrompt } = await setup({ mode: 'ptc', runtime: { language: 'python' } })
registerEcho(ctx)
const assembly = await systemPrompt.assemble()
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
@@ -442,7 +442,7 @@ describe('mode-aware wire contribution', () => {
// which throws when the schema is projected. Assembly's
// requireCodeRuntime rejects such a language earlier; this reaches the
// guard on its own.
- const { ctx } = await setup({ mode: 'code', runtime: { language: 'ruby' } })
+ const { ctx } = await setup({ mode: 'ptc', runtime: { language: 'ruby' } })
const definition = ctx.tools.get(RUN_CODE_NAME)
// Names the known languages, symmetric with the SDK_RENDERERS guard: this
// is the reachable rejection, so it must be at least as diagnosable.
@@ -453,19 +453,19 @@ describe('mode-aware wire contribution', () => {
it('degrades the run_code flavor to TypeScript when no runtime is mounted', async () => {
// Any reader of the definition without a mounted runtime uses this fallback; the
// shipped one is the tool-catalog generator, which boots the registry under
- // `mode: code` and reads run_code's schema WITHOUT a runtime. peekRuntime
+ // `mode: ptc` and reads run_code's schema WITHOUT a runtime. peekRuntime
// returns undefined there, so the flavor getter degrades to the TS default
// rather than throwing. None of those readers feeds a model: assembly goes
// through wireSchemas, which requires a runtime first.
- const { ctx } = await setup({ mode: 'code', runtime: false })
+ const { ctx } = await setup({ mode: 'ptc', runtime: false })
const definition = ctx.tools.get(RUN_CODE_NAME)
expect(definition?.description).toContain('Execute a TypeScript program')
const params = definition?.parameters as { properties: { code: { description: string } } }
expect(params.properties.code.description).toBe('The program: the body of an async TypeScript function.')
})
- it("rejects the assembly when toolOrder names a native tool that mode 'code' no longer contributes", async () => {
- const { ctx, systemPrompt } = await setup({ mode: 'code', toolOrder: ['echo', ''] })
+ it("rejects the assembly when toolOrder names a native tool that mode 'ptc' no longer contributes", async () => {
+ const { ctx, systemPrompt } = await setup({ mode: 'ptc', toolOrder: ['echo', ''] })
registerEcho(ctx)
await expect(systemPrompt.assemble()).rejects.toThrow(/toolOrder lists unregistered tool "echo"/)
})
@@ -474,7 +474,7 @@ describe('mode-aware wire contribution', () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(FakeRuntime, {})
- const fiber = await ctx.plugin(ToolRuntime, { mode: 'code' })
+ const fiber = await ctx.plugin(ToolRuntime, { mode: 'ptc' })
expect(ctx.tools.get(RUN_CODE_NAME)).toBeDefined()
await fiber.dispose()
const assembly = await ctx.systemPrompt.assemble()
@@ -520,7 +520,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
}
it('overlaps concurrency-safe calls under Promise.all and logs a start event per dispatch', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const gated = registerGated(ctx, 'safe_read', true)
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
@@ -548,7 +548,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
})
it('an exclusive call bars overlap: safe calls drain first, it runs alone, later calls wait', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const safe = registerGated(ctx, 'safe_read', true)
const unsafe = registerGated(ctx, 'writer', false)
runtime.behavior = async (request) => {
@@ -578,7 +578,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
})
it('maxParallelSubCalls caps the overlap window', async () => {
- const { ctx, runtime } = await setup({ mode: 'code', maxParallelSubCalls: 2 })
+ const { ctx, runtime } = await setup({ mode: 'ptc', maxParallelSubCalls: 2 })
const gated = registerGated(ctx, 'safe_read', true)
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
@@ -603,7 +603,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
})
it('a tool unregistered between binding enumeration and dispatch fails as unknown tool', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const calls: unknown[] = []
const dispose = ctx.tools.register(defineTool({
name: 'ephemeral',
@@ -635,7 +635,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
})
it('ordered pre-execute never overlaps: a slow policy on one call delays the next start', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const gated = registerGated(ctx, 'safe_read', true)
const stages: string[] = []
let releaseGate: (() => void) | undefined
@@ -671,7 +671,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
})
it('an exclusive call holds its barrier through post-execute: the next start waits for the commit', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const writer = registerGated(ctx, 'writer', false)
const reader = registerGated(ctx, 'safe_read', true)
const stages: string[] = []
@@ -708,7 +708,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
})
it('run settlement drains a commit already in progress: the settle event is appended inside the turn', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const gated = registerGated(ctx, 'safe_read', true)
const { agent, events } = fakeAgent()
let releasePost: (() => void) | undefined
@@ -742,7 +742,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
})
it('post-execute and context commitment stay in submission order under out-of-order completion', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const gated = registerGated(ctx, 'safe_read', true)
const postOrder: string[] = []
ctx.on('tools/post-execute', async (postExec, _result, next): Promise => {
@@ -778,7 +778,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
})
it('a queued-unstarted call abandoned by run settlement logs no start event', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const gated = registerGated(ctx, 'writer', false)
const { agent, events } = fakeAgent()
const abandoned: string[] = []
@@ -810,7 +810,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
describe('the run_code dispatch bridge', () => {
it('bridges tool calls, returns only the curated output, and logs one event per dispatch', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const calls = registerEcho(ctx)
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
@@ -841,7 +841,7 @@ describe('the run_code dispatch bridge', () => {
})
it('exposes only an opaque parent token to nested result observers', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
registerEcho(ctx)
runtime.behavior = async (request) => {
await request.bindings[0]!.functions.echo!({ value: 'nested' })
@@ -868,7 +868,7 @@ describe('the run_code dispatch bridge', () => {
})
it('forwards a nested terminal conclusion onto the successful run_code result', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
ctx.tools.register(defineTool({
name: 'finalize',
description: 'Terminal tool.',
@@ -908,7 +908,7 @@ describe('the run_code dispatch bridge', () => {
})
it('serializes Promise.all dispatches: tool executions never overlap, in submission order', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const intervals: [string, string][] = []
let active = 0
ctx.tools.register(defineTool({
@@ -946,7 +946,7 @@ describe('the run_code dispatch bridge', () => {
})
it('rejects the program-side call when the tool errors, with the tool error text', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
ctx.tools.register(defineContentToolFixture({
name: 'fail',
description: 'Always fails.',
@@ -965,10 +965,10 @@ describe('the run_code dispatch bridge', () => {
expect(result.content[0]).toEqual({ type: 'text', text: 'caught: deliberate failure' })
})
- it('a throwing tools/code-dispatch-log listener is contained: the original settled content is logged', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ it('a throwing tools/ptc-dispatch-log listener is contained: the original settled content is logged', async () => {
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
registerEcho(ctx)
- ctx.on('tools/code-dispatch-log', () => { throw new Error('log-content listener failed') })
+ ctx.on('tools/ptc-dispatch-log', () => { throw new Error('log-content listener failed') })
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
const value = await request.bindings[0]!.functions.echo!({ value: 'x' })
@@ -981,7 +981,7 @@ describe('the run_code dispatch bridge', () => {
})
it('a throwing tools/pre-execute listener settles the sub-call without post-execute', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const calls = registerEcho(ctx)
const postExecuted: string[] = []
ctx.on('tools/pre-execute', (exec, next) => {
@@ -1012,7 +1012,7 @@ describe('the run_code dispatch bridge', () => {
})
it('a tools/pre-execute deny reaches the program as a binding rejection', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
registerEcho(ctx)
ctx.on('tools/pre-execute', (exec, next) => {
if (exec.name === 'echo') return Promise.resolve({ kind: 'deny' as const, reason: 'not on my watch' })
@@ -1032,7 +1032,7 @@ describe('the run_code dispatch bridge', () => {
})
it('rejects a binding argument that is not lossless JSON, dispatching nothing', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const calls = registerEcho(ctx)
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
@@ -1050,7 +1050,7 @@ describe('the run_code dispatch bridge', () => {
})
it('dispatches and logs independent snapshots of the same lossless JSON value', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const calls = registerEcho(ctx)
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
@@ -1065,7 +1065,7 @@ describe('the run_code dispatch bridge', () => {
})
it('defers sub-call additionalContexts onto the outer run_code result', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
registerEcho(ctx)
ctx.on('tools/post-execute', (exec, _result, next): Promise => {
if (exec.name === 'echo') {
@@ -1101,7 +1101,7 @@ describe('the run_code dispatch bridge', () => {
})
it('defers image-bearing final sub-call content onto the outer run_code result', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
ctx.tools.register(defineContentToolFixture({
name: 'image_result',
description: 'Return one durable image.',
@@ -1136,7 +1136,7 @@ describe('the run_code dispatch bridge', () => {
it('does not defer images removed by a nested post-execute decision', async () => {
for (const decision of ['block', 'replace'] as const) {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
ctx.tools.register(defineContentToolFixture({
name: 'image_result',
description: 'Return one durable image.',
@@ -1197,7 +1197,7 @@ describe('the run_code dispatch bridge', () => {
})
it('converts a failed run into a structured isError result carrying kind, message, and captured logs', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
runtime.behavior = () => Promise.resolve({
logs: ['got this far'],
error: { kind: 'timeout', message: 'compute budget exhausted (300ms busy)' },
@@ -1218,7 +1218,7 @@ describe('the run_code dispatch bridge', () => {
})
it('aborting the outer signal aborts the in-flight sub-dispatch and abandons queued ones', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const seen: string[] = []
let sawAbort = false
ctx.tools.register(defineContentToolFixture({
@@ -1252,7 +1252,7 @@ describe('the run_code dispatch bridge', () => {
})
it('a runtime that starts a binding call and then REJECTS still reaches quiescence before returning', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const { agent, events } = fakeAgent()
let sawAbort = false
let started!: () => void
@@ -1287,7 +1287,7 @@ describe('the run_code dispatch bridge', () => {
})
it('runs without an owning agent: dispatches work, event logging is skipped', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const calls = registerEcho(ctx)
runtime.behavior = async (request) => {
await request.bindings[0]!.functions.echo!({ value: 'x' })
@@ -1301,14 +1301,14 @@ describe('the run_code dispatch bridge', () => {
it('executing run_code under a missing runtime is a structured isError, not a crash', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
- await ctx.plugin(ToolRuntime, { mode: 'code' })
+ await ctx.plugin(ToolRuntime, { mode: 'ptc' })
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(true)
expect((result.content[0] as { text: string }).text).toContain('requires a code runtime')
})
it('presents the model-authored description as the execute-card title over the program input', async () => {
- const { ctx } = await setup({ mode: 'code' })
+ const { ctx } = await setup({ mode: 'ptc' })
const tool = ctx.tools.get(RUN_CODE_NAME)!
// The description labels the card (the bash description precedent); the
// program itself remains the expanded raw input.
@@ -1321,7 +1321,7 @@ describe('the run_code dispatch bridge', () => {
})
it('rejects a whitespace-only description with a structured isError', async () => {
- const { ctx } = await setup({ mode: 'code' })
+ const { ctx } = await setup({ mode: 'ptc' })
const result = await runCode(ctx, 'return 1', { description: ' ' })
expect(result.isError).toBe(true)
expect((result.content[0] as { text: string }).text).toContain('invalid description')
@@ -1333,7 +1333,7 @@ describe('the run_code dispatch bridge', () => {
['logs plus result', { logs: ['printed'], value: 'returned' }, 'printed\nreturned'],
['no output', { logs: [] }, '(run_code completed with no output)'],
] as [string, CodeRunResult, string][])('keeps %s in durable content without a result presenter', async (_name, output, text) => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
runtime.behavior = () => Promise.resolve(output)
const result = await runCode(ctx, 'return 1')
@@ -1347,7 +1347,7 @@ describe('the run_code dispatch bridge', () => {
})
it('keeps a post-policy spill preview in durable content without a result presenter', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const preview = 'HEAD\n\n(Omitted 100 bytes. Full formatted result stored at: /tmp/run-code.txt.)\n\nTAIL'
runtime.behavior = () => Promise.resolve({ logs: ['printed'], value: 'returned' })
ctx.on('tools/post-execute', (exec, _result, next): Promise => {
@@ -1363,7 +1363,7 @@ describe('the run_code dispatch bridge', () => {
})
it('keeps canonical failure content durable without a result presenter', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
runtime.behavior = () => Promise.resolve({
logs: ['captured before failure'],
error: { kind: 'output-limit', message: 'outer output exceeded 8 bytes' },
@@ -1381,7 +1381,7 @@ describe('the run_code dispatch bridge', () => {
})
it('logs the complete sub-result content verbatim, non-text blocks and long text included', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const { agent, events } = fakeAgent()
const long = 'x'.repeat(300)
ctx.tools.register(defineTool({
@@ -1414,7 +1414,7 @@ describe('the run_code dispatch bridge', () => {
})
it('rejects undefined, getter-throwing, exotic, and unrepresentable binding arguments before dispatch', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const calls = registerEcho(ctx)
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
@@ -1446,7 +1446,7 @@ describe('the run_code dispatch bridge', () => {
})
it('dispatches and durably logs binding arguments deeper than the structured-clone call stack', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const depth = 5_000
let observedDepth = 0
let observedLeaf: JsonValue | undefined
@@ -1497,7 +1497,7 @@ describe('the run_code dispatch bridge', () => {
})
it('gives the tool and durable log the same immutable argument value', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const { agent, events } = fakeAgent()
let mutationSucceeded: boolean | undefined
ctx.tools.register(defineContentToolFixture({
@@ -1521,7 +1521,7 @@ describe('the run_code dispatch bridge', () => {
})
it('exposes a tool named __proto__ as an ordinary own binding', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
ctx.tools.register(defineTool({
name: '__proto__',
description: 'A prototype-colliding tool name.',
@@ -1544,7 +1544,7 @@ describe('the run_code dispatch bridge', () => {
})
it('renders every non-string JSON root as pretty JSON while preserving strings raw', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
runtime.behavior = () => Promise.resolve({ logs: [], value: { n: 42, ok: true } })
expect((await runCode(ctx, 'object')).content[0]).toEqual({ type: 'text', text: '{\n "n": 42,\n "ok": true\n}' })
runtime.behavior = () => Promise.resolve({ logs: [], value: {} })
@@ -1567,7 +1567,7 @@ describe('the run_code dispatch bridge', () => {
})
it('renders deeply nested JSON without recursive traversal or quadratic indentation', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
let value: JsonValue = {
emptyArray: [],
emptyObject: {},
@@ -1588,7 +1588,7 @@ describe('the run_code dispatch bridge', () => {
})
it('short-circuits a pre-aborted outer signal before the code runtime', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const calls = registerEcho(ctx)
runtime.behavior = (request) => {
// The fake honors the seam contract for an already-aborted signal.
@@ -1612,7 +1612,7 @@ describe('the run_code dispatch bridge', () => {
})
it('reports cancellation after rejecting a late binding without dispatching it', async () => {
- const { ctx, runtime } = await setup({ mode: 'code' })
+ const { ctx, runtime } = await setup({ mode: 'ptc' })
const calls = registerEcho(ctx)
const controller = new AbortController()
runtime.behavior = async (request) => {
@@ -1632,7 +1632,7 @@ describe('the run_code dispatch bridge', () => {
})
it('a tool/code-dispatch event never derives a model message', () => {
- const session = Session.create(SessionId('code-mode-derive'))
+ const session = Session.create(SessionId('ptc-derive'))
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
@@ -1653,14 +1653,14 @@ describe('the run_code dispatch bridge', () => {
it('direct construction rejects a non-positive parallel sub-call cap at load', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
- expect(() => new ToolRuntime(ctx, { mode: 'code', maxParallelSubCalls: 0 }))
+ expect(() => new ToolRuntime(ctx, { mode: 'ptc', maxParallelSubCalls: 0 }))
.toThrow('maxParallelSubCalls must be a positive integer')
})
- it('direct construction in code mode defaults the parallel sub-call cap', async () => {
+ it('direct construction in PTC mode defaults the parallel sub-call cap', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
- const registry = new ToolRuntime(ctx, { mode: 'code' })
+ const registry = new ToolRuntime(ctx, { mode: 'ptc' })
expect(registry.get(RUN_CODE_NAME)).toBeDefined()
})
@@ -1672,10 +1672,10 @@ describe('the run_code dispatch bridge', () => {
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
})
- it('denies a model-direct native-tool call under code mode as UNKNOWN_TOOL', async () => {
+ it('denies a model-direct native-tool call under PTC mode as UNKNOWN_TOOL', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
- const registry = new ToolRuntime(ctx, { mode: 'code' })
+ const registry = new ToolRuntime(ctx, { mode: 'ptc' })
registerEcho(ctx, 'write')
const result = await registry.execute({
signal: testToolSignal,
@@ -1695,7 +1695,7 @@ describe('the run_code dispatch bridge', () => {
it('routes a pre-aborted collapsed call through ABORTED_BEFORE_DISPATCH', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
- const registry = new ToolRuntime(ctx, { mode: 'code' })
+ const registry = new ToolRuntime(ctx, { mode: 'ptc' })
registerEcho(ctx, 'write')
const aborted = new AbortController()
aborted.abort()
@@ -1713,17 +1713,17 @@ describe('the run_code dispatch bridge', () => {
/**
* Presentation is per agent, because an agent preset composes it: one
- * deployment runs a Code Mode agent beside native ones, and neither may see
+ * deployment runs a PTC mode agent beside native ones, and neither may see
* the other's catalog. The deployment `mode` is the default those agents
* shadow, not a process-wide fact.
*/
describe('per-agent presentation', () => {
- it('gives one agent Code Mode while the deployment stays native', async () => {
+ it('gives one agent PTC mode while the deployment stays native', async () => {
const { ctx, systemPrompt } = await setup({ mode: 'native' })
const calls = registerEcho(ctx)
const { scope, agent } = await mintAgentScope(ctx)
- scope.ctx.tools.presentAs('code')
+ scope.ctx.tools.presentAs('ptc')
const coded = await systemPrompt.assemble({ scope: agent })
expect(coded.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
@@ -1753,8 +1753,8 @@ describe('per-agent presentation', () => {
const calls = registerEcho(ctx)
// The preset's standing scope declares once; the agent only PARENTS to it
// (the per-preset standing mount configuration has no per-agent declaration).
- const standing = await mintAgentScope(ctx, 'preset:code-like')
- standing.scope.ctx.tools.presentAs('code')
+ const standing = await mintAgentScope(ctx, 'preset:ptc-like')
+ standing.scope.ctx.tools.presentAs('ptc')
const joined = await mintAgentScope(ctx, 'joined-agent')
bindScopeParent(joined.agent, standing.agent)
const loner = await mintAgentScope(ctx, 'loner-agent')
@@ -1803,7 +1803,7 @@ describe('per-agent presentation', () => {
registerEcho(ctx)
const coded = await mintAgentScope(ctx, 'coded')
const plain = await mintAgentScope(ctx, 'plain')
- coded.scope.ctx.tools.presentAs('code')
+ coded.scope.ctx.tools.presentAs('ptc')
// Not merely hidden from the prompt: the transport one agent presents must
// not be dispatchable by another that never presented it.
@@ -1812,8 +1812,8 @@ describe('per-agent presentation', () => {
expect(ctx.tools.get(RUN_CODE_NAME)).toBeUndefined()
})
- it('lets an agent opt out of a code-mode deployment', async () => {
- const { ctx, systemPrompt } = await setup({ mode: 'code' })
+ it('lets an agent opt out of a PTC mode deployment', async () => {
+ const { ctx, systemPrompt } = await setup({ mode: 'ptc' })
registerEcho(ctx)
const { scope, agent } = await mintAgentScope(ctx)
@@ -1830,7 +1830,7 @@ describe('per-agent presentation', () => {
const { ctx, systemPrompt } = await setup({ mode: 'native' })
registerEcho(ctx)
const { scope, agent } = await mintAgentScope(ctx)
- const dispose = scope.ctx.tools.presentAs('code')
+ const dispose = scope.ctx.tools.presentAs('ptc')
dispose()
@@ -1842,18 +1842,18 @@ describe('per-agent presentation', () => {
it('refuses a second declaration for the same agent', async () => {
const { ctx } = await setup({ mode: 'native' })
const { scope } = await mintAgentScope(ctx)
- scope.ctx.tools.presentAs('code')
+ scope.ctx.tools.presentAs('ptc')
// Two answers to "which form does the model see" is a contradiction, and
// silently keeping either one would make the composition unreadable.
expect(() => scope.ctx.tools.presentAs('both'))
- .toThrow('conflicts with "code" already declared')
+ .toThrow('conflicts with "ptc" already declared')
})
it('refuses an unscoped declaration', async () => {
const { ctx } = await setup({ mode: 'native' })
- expect(() => ctx.tools.presentAs('code'))
+ expect(() => ctx.tools.presentAs('ptc'))
.toThrow('requires a scoped context')
})
diff --git a/packages/core/tools/tests/py-types.spec.ts b/packages/core/tools/tests/py-types.spec.ts
index 0caf9daab6..92fc269332 100644
--- a/packages/core/tools/tests/py-types.spec.ts
+++ b/packages/core/tools/tests/py-types.spec.ts
@@ -428,7 +428,7 @@ describe('renderToolsSdkPy', () => {
// `路径` satisfies `xid_start xid_continue*`, so CPython accepts it as an
// attribute and as the `TypedDict` key. Rejecting it would degrade the
// whole object, dropping every SIBLING field's name, requiredness and type
- // too — and under `mode: 'code'` the native schemas are omitted, so
+ // too — and under `mode: 'ptc'` the native schemas are omitted, so
// nothing else carries them. The nested class name is from the field, so
// `camelCase` has to pass the same characters through instead of splitting
// on them.
@@ -1037,7 +1037,7 @@ describe('renderToolsSdkPy', () => {
// `__debug__` is a legal identifier and dunder-form, so it clears both the
// identifier rule and the name-mangling rule, but CPython rejects the
// annotation at COMPILE time (`SyntaxError: cannot assign to __debug__`) —
- // and this block is Code Mode's only SDK, so it must always parse.
+ // and this block is PTC mode's only SDK, so it must always parse.
const t: ToolSdkSchema = {
name: 'debugger',
description: '',
@@ -1111,7 +1111,7 @@ describe('renderToolsSdkPy', () => {
// `compile()` raises `SyntaxError: source code string cannot contain null
// bytes` for a NUL ANYWHERE in the source text, including inside a string
// literal or a comment, so a NUL that survives normalization into a
- // docstring or a `#` field comment stops this block — Code Mode's only SDK —
+ // docstring or a `#` field comment stops this block — PTC mode's only SDK —
// from parsing at all. The whitespace collapse does not remove it (a NUL is
// not whitespace). Rendering it as a visible escape keeps the source
// parseable and still shows the model what the schema said.
@@ -1156,7 +1156,7 @@ describe('renderToolsSdkPy', () => {
// This is the NUL case, not the invisible-character case: Python source
// must be UTF-8-encodable, and `compile()` raises `UnicodeEncodeError:
// surrogates not allowed` for a lone surrogate in a string literal and in a
- // `#` comment alike, so one would stop this block — Code Mode's only SDK —
+ // `#` comment alike, so one would stop this block — PTC mode's only SDK —
// from parsing. A wire description reaches it: `JSON.parse` on a `"\ud800"`
// escape yields exactly this code point.
const high = renderToolsSdkPy([described('a\ud800b')])
diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts
index 15f7203cfa..2510739cbb 100644
--- a/packages/core/tools/tests/tools.spec.ts
+++ b/packages/core/tools/tests/tools.spec.ts
@@ -1,8 +1,11 @@
import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
-import { createUserMessage, ToolCallId, HarnessError, type ContentBlock } from '@deepseek-ai/dsh-llm'
+import LlmRuntime, { createUserMessage, ToolCallId, HarnessError, type ContentBlock } from '@deepseek-ai/dsh-llm'
+import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
-import type { Agent } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
+import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
import ToolRuntime, {
defineContentToolFixture, defineTool, JsonSchemaError, parameterSchemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError,
@@ -718,19 +721,21 @@ describe('ToolRuntime', () => {
})
describe('ask routing through ctx.approval', () => {
- /**
- * A minimal Agent stand-in — the approval seam reaches
- * `agent.session.append` and folds `.events`; the seeded open turn
- * satisfies request()'s enclosure precondition.
- */
function fakeAgent(): Agent {
- return {
- session: { events: [{ type: 'turn/start' }], append: () => ({}) },
- } as unknown as Agent
+ const session = Session.create(SessionId('approval-fake-agent'))
+ session.append('turn/start', { turn: 1 })
+ return { session } as unknown as Agent
}
async function approvalSetup() {
- const ctx = await setup()
+ const ctx = new Context()
+ await ctx.plugin(LlmRuntime)
+ await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(SystemPrompt)
+ await ctx.plugin(ToolRuntime)
+ await ctx.plugin(AgentRegistry)
+ await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(ApprovalService)
ctx.tools.register(echoTool)
return ctx
diff --git a/packages/credentials/authorization/package.json b/packages/credentials/authorization/package.json
index c88162f140..60d4b55830 100644
--- a/packages/credentials/authorization/package.json
+++ b/packages/credentials/authorization/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-authorization",
"description": "Authorization seam (ctx.authorization): plugin-owned flows that obtain a credential through a conversation with the human",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/credentials/credentials-local/package.json b/packages/credentials/credentials-local/package.json
index 5d802e76e2..22ad915e46 100644
--- a/packages/credentials/credentials-local/package.json
+++ b/packages/credentials/credentials-local/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-credentials-local",
"description": "File-backed credentials provider ($DSH_HOME/.env under the live process environment) for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/credentials/credentials/package.json b/packages/credentials/credentials/package.json
index 13a5f0f794..35102c4603 100644
--- a/packages/credentials/credentials/package.json
+++ b/packages/credentials/credentials/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-credentials",
"description": "Abstract credential seam (ctx.credentials): settings carry references to secrets, providers own the values",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/e2b/e2b/package.json b/packages/e2b/e2b/package.json
index e8e979c880..a94691bbfe 100644
--- a/packages/e2b/e2b/package.json
+++ b/packages/e2b/e2b/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-e2b",
"description": "Shared E2B sandbox lifecycle for DeepSeek Harness provider adapters",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -50,6 +50,7 @@
"@deepseek-ai/dsh-lsp-stdio": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-subprocess-e2b": "workspace:^",
"@deepseek-ai/dsh-terminal": "workspace:^",
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
diff --git a/packages/e2b/e2b/tests/composition.e2e.ts b/packages/e2b/e2b/tests/composition.e2e.ts
index 55dcf93b08..dbbdff065e 100644
--- a/packages/e2b/e2b/tests/composition.e2e.ts
+++ b/packages/e2b/e2b/tests/composition.e2e.ts
@@ -14,6 +14,7 @@ import {
import TerminalSessionService, { TerminalSessionId } from '@deepseek-ai/dsh-terminal'
import { BashTerminalBackend } from '@deepseek-ai/dsh-terminal-bash'
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import E2BSubprocessRuntime from '@deepseek-ai/dsh-subprocess-e2b'
@@ -52,6 +53,7 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => {
runtimeRoot: '/home/user/.dsh-e2b',
getSandbox: async () => sandbox,
} as never)
+ await ctx.plugin(SessionProjectionRegistry)
const sandboxPolicyFiber = await ctx.plugin(SandboxPolicyService, {
mode: 'danger-full-access',
workspaceRoot: '/home/user',
diff --git a/packages/e2b/e2b/tests/fixtures/composition/cordis.yml b/packages/e2b/e2b/tests/fixtures/composition/cordis.yml
index 618baa8e10..6177630e25 100644
--- a/packages/e2b/e2b/tests/fixtures/composition/cordis.yml
+++ b/packages/e2b/e2b/tests/fixtures/composition/cordis.yml
@@ -21,6 +21,9 @@
- id: agents
name: '@deepseek-ai/dsh-agent'
+- id: session-projection
+ name: '@deepseek-ai/dsh-session-projection'
+
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
diff --git a/packages/e2b/fs-e2b/package.json b/packages/e2b/fs-e2b/package.json
index ab88b3e5c5..d2dce5c4c7 100644
--- a/packages/e2b/fs-e2b/package.json
+++ b/packages/e2b/fs-e2b/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-fs-e2b",
"description": "E2B filesystem implementation for DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/e2b/subprocess-e2b/package.json b/packages/e2b/subprocess-e2b/package.json
index 25d98bfe39..c05d938c21 100644
--- a/packages/e2b/subprocess-e2b/package.json
+++ b/packages/e2b/subprocess-e2b/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-subprocess-e2b",
"description": "E2B subprocess implementation for DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/examples/agent-spine-demo/README.i18n.yaml b/packages/examples/agent-spine-demo/README.i18n.yaml
index bbaf2ec214..d6dd65615e 100644
--- a/packages/examples/agent-spine-demo/README.i18n.yaml
+++ b/packages/examples/agent-spine-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/agent-spine-demo/README.md
-README.md: 54114f1f5bb6900a0e96c09730b2fe88ceb92f20
-README.zh.md: 2dbc244cec7744f33da85c6d4361f3f9acc4d8b7
+README.md: 727effb9f6c9c4b1b5af6c7f3da8aea50b0641fa
+README.zh.md: df84c48e8271cdddcbf68e7659e8014e0c223ae1
diff --git a/packages/examples/agent-spine-demo/README.md b/packages/examples/agent-spine-demo/README.md
index 54114f1f5b..727effb9f6 100644
--- a/packages/examples/agent-spine-demo/README.md
+++ b/packages/examples/agent-spine-demo/README.md
@@ -64,7 +64,7 @@ The smallest working setup mounts the bundle with a workspace-context budget, pl
| `includeRuntimeContext` | `true` | whether the agent's history includes dynamic runtime-context snapshots |
| `persona` | `''` | the deployment persona text in the system prompt |
| `toolOrder` | lexicographic | the order the model sees tools in |
-| `tools` | `{ mode: 'native' }` | how tools reach the model: native schemas, Code Mode, or both |
+| `tools` | `{ mode: 'native' }` | how tools reach the model: native schemas, PTC mode, or both |
| `dshHome` | `$DSH_HOME` or `~/.dsh` | the harness home used for the bash environment and local skill folders |
| `sessionTitle` | example limits | fallback title limits: 5 words, 40 fallback bytes, 80 accepted bytes |
| `workspaceContext` | required | byte budget for loading workspace files into context, or `false` |
diff --git a/packages/examples/agent-spine-demo/README.zh.md b/packages/examples/agent-spine-demo/README.zh.md
index 2dbc244cec..df84c48e82 100644
--- a/packages/examples/agent-spine-demo/README.zh.md
+++ b/packages/examples/agent-spine-demo/README.zh.md
@@ -64,7 +64,7 @@ kind: "package-reference"
| `includeRuntimeContext` | `true` | agent 历史是否包含动态运行时上下文快照 |
| `persona` | `''` | 系统提示词中的部署 persona 文本 |
| `toolOrder` | 字典序 | 模型看到工具的顺序 |
-| `tools` | `{ mode: 'native' }` | 工具如何到达模型:native schema、Code Mode 或两者 |
+| `tools` | `{ mode: 'native' }` | 工具如何到达模型:native schema、PTC mode 或两者 |
| `dshHome` | `$DSH_HOME` 或 `~/.dsh` | bash 环境与本地 skill 目录使用的 harness 主目录 |
| `sessionTitle` | 示例限制 | 后备标题限制:5 个词、40 个后备字节、80 个可接受字节 |
| `workspaceContext` | 必填 | 加载工作区文件进上下文的字节预算,或 `false` |
diff --git a/packages/examples/agent-spine-demo/package.json b/packages/examples/agent-spine-demo/package.json
index 2d19e61ff5..fdea770f50 100644
--- a/packages/examples/agent-spine-demo/package.json
+++ b/packages/examples/agent-spine-demo/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-spine-demo",
"description": "The default executor-less/UI-less agent spine with fallback session titles, provider-routed retry, and optional persisted goals",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -92,7 +92,8 @@
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-agent-instructions": "workspace:^",
"@deepseek-ai/node-addon-landlock-run": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^"
diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts
index 5dafbd363a..f74bc9407c 100644
--- a/packages/examples/agent-spine-demo/src/index.ts
+++ b/packages/examples/agent-spine-demo/src/index.ts
@@ -13,6 +13,7 @@ import Timer from '@deepseek-ai/cordis-plugin-timer'
import z from '@deepseek-ai/schemastery'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionTitleService, { type Config as SessionTitleConfig } from '@deepseek-ai/dsh-session-title'
import SystemPrompt, { type Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
@@ -221,6 +222,7 @@ export function apply(ctx: Context, config: Config): void {
ctx.plugin(Timer)
ctx.plugin(LlmRuntime)
ctx.plugin(SessionStore)
+ ctx.plugin(SessionProjectionRegistry)
ctx.plugin(SessionTitleService, config.sessionTitle ?? EXAMPLE_SESSION_TITLE_CONFIG)
// Owner schemas resolve defaults; forward toolOrder only when explicitly set.
ctx.plugin(SystemPrompt, {
diff --git a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts
index 1b4a541901..94d16b50fe 100644
--- a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts
+++ b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts
@@ -8,6 +8,7 @@ import { renderPrompt, TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import * as agentCore from '../src/index.ts'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
@@ -77,6 +78,9 @@ async function mount(config: agentCore.Config, withBash = false): Promise {
// ctx.plugin validates + defaults the bundle config first; a direct apply
// skips the schema, so the forwarding `?? []` / `?? ''` are what fire.
const ctx = new Context()
+ await ctx.plugin(SessionProjectionRegistry)
agentCore.apply(ctx, { workspaceContext: false })
await new Promise(resolve => setTimeout(resolve, 50))
expect(ctx.get('agentLoop')).toBeDefined()
@@ -337,6 +342,7 @@ describe('dsh-agent-spine-demo bundle', () => {
it('uses owner defaults for a schema-bypassing empty goal opt-in', async () => {
const ctx = new Context()
+ await ctx.plugin(SessionProjectionRegistry)
agentCore.apply(ctx, {
workspaceContext: false,
agents: [{ id: SessionId('defaulted-goal'), provider: 'mock', model: 'mock' }],
@@ -772,6 +778,7 @@ describe('dsh-agent-spine-demo bundle', () => {
it('uses the default skill config when apply is called directly without skills', async () => {
await withIsolatedSkillHomes(async () => {
const ctx = new Context()
+ await ctx.plugin(SessionProjectionRegistry)
agentCore.apply(ctx, { agents: [], workspaceContext: false })
await new Promise(resolve => setTimeout(resolve, 50))
expect(ctx.skills).toBeDefined()
@@ -800,6 +807,7 @@ describe('dsh-agent-spine-demo bundle', () => {
it('supports direct apply with workspace instructions disabled and no forwarded agents', async () => {
const ctx = new Context()
+ await ctx.plugin(SessionProjectionRegistry)
agentCore.apply(ctx, { workspaceContext: false })
await new Promise(resolve => setTimeout(resolve, 50))
diff --git a/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts b/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts
index 8bc735f3f6..613ebb2aec 100644
--- a/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts
+++ b/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts
@@ -12,6 +12,7 @@ import { ToolCallId } from '@deepseek-ai/dsh-llm'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { bwrapProfileArgs, seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { SessionId } from '@deepseek-ai/dsh-session'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import type { ToolResult } from '@deepseek-ai/dsh-tools'
@@ -50,6 +51,7 @@ beforeEach(async () => {
ctx = new Context()
await ctx.plugin(LocalSandboxProvider, {})
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: fallbackRoot })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(SandboxBashExecutor, { cwd: fallbackRoot, timeoutMs: 30_000 })
diff --git a/packages/experimental/agent-team-profile/package.json b/packages/experimental/agent-team-profile/package.json
index d18f2ebebb..fcdfa983ee 100644
--- a/packages/experimental/agent-team-profile/package.json
+++ b/packages/experimental/agent-team-profile/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-experimental-agent-team-profile",
"description": "Private profile bundle enabling Agent Teams over dsh-base",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"private": true,
"repository": {
"type": "git",
diff --git a/packages/experimental/agent-team-web-profile/package.json b/packages/experimental/agent-team-web-profile/package.json
index bc0e0c36b9..0b29840755 100644
--- a/packages/experimental/agent-team-web-profile/package.json
+++ b/packages/experimental/agent-team-web-profile/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-experimental-agent-team-web-profile",
"description": "Private Web profile layer for Agent Teams Remote and UI plugins",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"private": true,
"repository": {
"type": "git",
diff --git a/packages/experimental/agent-team/README.i18n.yaml b/packages/experimental/agent-team/README.i18n.yaml
index 508a041176..f4ee635c11 100644
--- a/packages/experimental/agent-team/README.i18n.yaml
+++ b/packages/experimental/agent-team/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/experimental/agent-team/README.md
-README.md: bac73d873ec0856fa2b0550a7f25331c5d7035fb
-README.zh.md: 2981e611a48736124dda79a8721e01c11fbcf7e6
+README.md: 0316834f566112b708f513b46cd0e5e3ab503061
+README.zh.md: 0aae14bccc06c57a9e1698f302cadd6b293baca8
diff --git a/packages/experimental/agent-team/README.md b/packages/experimental/agent-team/README.md
index bac73d873e..0316834f56 100644
--- a/packages/experimental/agent-team/README.md
+++ b/packages/experimental/agent-team/README.md
@@ -118,7 +118,7 @@ The [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-
| [`src/mailbox.ts`](src/mailbox.ts) | Durable queue, target-local dispatch, acknowledgement, and recovery |
| [`src/task-board.ts`](src/task-board.ts) | Task CAS commands, DAG validation, and derived views |
| [`src/journal.ts`](src/journal.ts) | Serialized Lead-log transactions and commit notification |
-| [`src/fold.ts`](src/fold.ts) | Strict replay fold that decodes and validates Team events |
+| [`src/projection.ts`](src/projection.ts) | Strict replay projection that decodes and validates Team events |
| [`src/activity.ts`](src/activity.ts) | One-shot change waiters and disposal release |
| [`src/lifecycle.ts`](src/lifecycle.ts) | Shared admission cutoff and bounded settlement |
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion that replays candidate events before append |
diff --git a/packages/experimental/agent-team/README.zh.md b/packages/experimental/agent-team/README.zh.md
index 2981e611a4..0aae14bccc 100644
--- a/packages/experimental/agent-team/README.zh.md
+++ b/packages/experimental/agent-team/README.zh.md
@@ -118,7 +118,7 @@ Lead 可以停止 teammate 的当前轮次,而不会删除其排队的消息
| [`src/mailbox.ts`](src/mailbox.ts) | 持久队列、目标本地投递、确认与恢复 |
| [`src/task-board.ts`](src/task-board.ts) | 任务 CAS 命令、DAG 校验与派生视图 |
| [`src/journal.ts`](src/journal.ts) | 串行化的 Lead 日志事务与提交通知 |
-| [`src/fold.ts`](src/fold.ts) | 解码并校验 Team 事件的严格回放折叠 |
+| [`src/projection.ts`](src/projection.ts) | 解码并校验 Team 事件的严格回放投影 |
| [`src/activity.ts`](src/activity.ts) | 一次性变更等待者与 dispose 释放 |
| [`src/lifecycle.ts`](src/lifecycle.ts) | 共享准入截止与有界结算 |
| [`src/invariant.ts`](src/invariant.ts) | 在 append 前回放候选事件的不变式伴生插件 |
diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json
index 1aa07e6c1e..038739deb9 100644
--- a/packages/experimental/agent-team/package.json
+++ b/packages/experimental/agent-team/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-experimental-agent-team",
"description": "Implicit-root Agent Teams roster, durable peer mailbox, and shared task DAG",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"private": true,
"repository": {
"type": "git",
@@ -61,6 +61,7 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
@@ -73,6 +74,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-session-persistence-sqlite": "workspace:^",
diff --git a/packages/experimental/agent-team/src/index.ts b/packages/experimental/agent-team/src/index.ts
index a207cd2da4..de0487e93b 100644
--- a/packages/experimental/agent-team/src/index.ts
+++ b/packages/experimental/agent-team/src/index.ts
@@ -10,6 +10,7 @@ import { errorMessage, TeamError } from './error.ts'
import { TeamJournal } from './journal.ts'
import { TeamRuntimeLifecycle } from './lifecycle.ts'
import { TeamMailbox } from './mailbox.ts'
+import { teamProjectionDefinition } from './projection.ts'
import { TeamRoster } from './roster.ts'
import type { TeamMembership } from './roster.ts'
import { TeamTaskBoard } from './task-board.ts'
@@ -33,7 +34,6 @@ export type * from './types.ts'
export type { TeamMembership } from './roster.ts'
export { TeamId, TeamMessageId, TeamTaskId } from './types.ts'
export { TeamError } from './error.ts'
-export { foldTeam } from './fold.ts'
declare module '@deepseek-ai/cordis' {
interface Context {
@@ -57,7 +57,7 @@ function positiveLimit(name: string, value: number): number {
/** Agent Teams service backed by the exact live Lead Session log. */
export class TeamService extends TypertRemoteService {
- static inject = ['agents', 'sessions', 'sessionPersistence', 'subagents']
+ static inject = ['agents', 'sessions', 'sessionPersistence', 'sessionProjections', 'subagents']
static Config: z = z.object({
maxMembers: z.number().step(1).min(1).default(DEFAULT_MAX_MEMBERS),
@@ -113,7 +113,16 @@ export class TeamService extends TypertRemoteService {
const membership = this.roster.tryMembership(agent)
if (membership !== undefined) this.activity.notify(membership.id)
})
- ctx.effect(() => () => this.disposeRuntime(), 'agentTeams.runtimeLifecycle()')
+ ctx.effect(() => {
+ const disposeProjection = ctx.root.sessionProjections.register(teamProjectionDefinition)
+ return async () => {
+ try {
+ await this.disposeRuntime()
+ } finally {
+ disposeProjection()
+ }
+ }
+ }, 'agentTeams.runtimeLifecycle()')
for (const agent of ctx.agents.list()) this.scheduleRecovery(agent)
}
diff --git a/packages/experimental/agent-team/src/invariant.ts b/packages/experimental/agent-team/src/invariant.ts
index 71fae13dbc..dc5fd3ddc1 100644
--- a/packages/experimental/agent-team/src/invariant.ts
+++ b/packages/experimental/agent-team/src/invariant.ts
@@ -1,9 +1,13 @@
-/** Package-owned relational checks for Agent Teams durable records. */
+/** Agent Teams runtime invariant companion. */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
-import { applyTeamEvent, foldTeam, isTeamEvent } from './fold.ts'
+import {
+ isTeamEvent,
+ teamProjectionDefinition,
+ type TeamProjectionState,
+} from './projection.ts'
const PACKAGE_NAME = '@deepseek-ai/dsh-experimental-agent-team'
@@ -12,22 +16,20 @@ export const name = 'team-invariant'
/** Invariant registry required by the companion. */
export const inject = ['invariants']
-/** Validate candidate Team events against the committed prefix before append. */
+/** Validate candidate Team events against the projected committed prefix. */
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
ctx.on('internal/dispatch', (_mode, eventName, args) => {
if (eventName !== 'session/event') return
const [session, event] = args as [Session, SessionEvent]
+ /* v8 ignore next -- non-Team Session events have no Agent Teams invariant. */
if (!isTeamEvent(event)) return
- try {
- const state = foldTeam(session.id, session.events)
- applyTeamEvent(state, event)
- } catch (error: unknown) {
- /* v8 ignore next -- the strict Team fold throws Error instances. */
- const message = error instanceof Error ? error.message : String(error)
- fail(`session event ${event.seq} violates the Agent Teams stream: ${message}`)
+ const state = ctx.sessionProjections.stateOf(session, 'agentTeam') as TeamProjectionState
+ const candidate = teamProjectionDefinition.apply(structuredClone(state), event)
+ if (candidate.failure !== undefined) {
+ fail(`session event ${event.seq} violates the Agent Teams stream: ${candidate.failure}`)
}
}, { global: true })
-}, { inject: ['sessions'] })
+}, { inject: ['sessionProjections'] })
/** Register the package invariant companion. */
export const apply = (ctx: Context): Promise<() => void> =>
diff --git a/packages/experimental/agent-team/src/journal.ts b/packages/experimental/agent-team/src/journal.ts
index f97e7776f9..25d8369d30 100644
--- a/packages/experimental/agent-team/src/journal.ts
+++ b/packages/experimental/agent-team/src/journal.ts
@@ -3,8 +3,7 @@
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { Context } from '@deepseek-ai/cordis'
import type { SessionEventMap, SessionId } from '@deepseek-ai/dsh-session'
-import { foldTeam } from './fold.ts'
-import type { TeamEventType, TeamFoldState } from './fold.ts'
+import type { TeamEventType, TeamState } from './projection.ts'
type AppendTeamEvent = (type: T, data: SessionEventMap[T]) => void
type MutableTeamEventType = 'team/member' | 'team/task' | 'team/message/queued' | 'team/message/delivered'
@@ -23,12 +22,15 @@ export class TeamJournal {
) {}
/**
- * Fold authoritative Team state for one exact live Lead.
+ * Read authoritative Team state for one exact live Lead.
* @param root - exact live Team Lead.
- * @returns current replay state selected by the Lead Team id.
+ * @returns current projected state selected by the Lead Team id.
*/
- state(root: Agent): TeamFoldState {
- return foldTeam(root.id, root.session.events)
+ state(root: Agent): TeamState {
+ const projection = this.ctx.sessionProjections.stateOf(root.session, 'agentTeam')
+ if (projection === undefined) throw new Error('Agent Teams projection is not registered')
+ if (projection.failure !== undefined) throw new Error(projection.failure)
+ return projection
}
/**
diff --git a/packages/experimental/agent-team/src/mailbox.ts b/packages/experimental/agent-team/src/mailbox.ts
index b234cef408..436e494fb1 100644
--- a/packages/experimental/agent-team/src/mailbox.ts
+++ b/packages/experimental/agent-team/src/mailbox.ts
@@ -86,8 +86,8 @@ export class TeamMailbox {
const membership = this.roster.tryMembership(agent)
if (membership === undefined) return
const state = this.journal.state(membership.root)
- const messages = [...state.messages.values()].filter(message =>
- !state.delivered.has(message.id)
+ const messages = state.messages.filter(message =>
+ !state.delivered.includes(message.id)
&& (membership.role === 'lead' || message.targetId === agent.id))
for (const message of messages) {
signal.throwIfAborted()
@@ -119,8 +119,8 @@ export class TeamMailbox {
const state = this.journal.state(root)
const target = resolveActiveMember(root, state, request.target)
if (target.id === caller.id) throw new TeamError('a Team member cannot message itself', 'TEAM_SELF_MESSAGE')
- const pendingForTarget = [...state.messages.values()].filter(candidate =>
- candidate.targetId === target.id && !state.delivered.has(candidate.id)).length
+ const pendingForTarget = state.messages.filter(candidate =>
+ candidate.targetId === target.id && !state.delivered.includes(candidate.id)).length
if (pendingForTarget >= this.maxPendingMessagesPerMember) {
throw new TeamError(
`teammate "${target.name}" has ${pendingForTarget} pending messages`,
@@ -272,7 +272,7 @@ export class TeamMailbox {
/** Whether `left` was durably queued before `right` in one Lead log. */
private messagePrecedes(root: Agent, left: TeamMessageId, right: TeamMessageId): boolean {
- const ids = [...this.journal.state(root).messages.keys()]
+ const ids = this.journal.state(root).messages.map(message => message.id)
return ids.indexOf(left) < ids.indexOf(right)
}
@@ -292,8 +292,8 @@ export class TeamMailbox {
private async markDelivered(root: Agent, messageId: TeamMessageId, targetId: SessionId): Promise {
await this.journal.transact(root.id, async () => {
const state = this.journal.state(root)
- if (state.delivered.has(messageId)) return
- const queued = state.messages.get(messageId)
+ if (state.delivered.includes(messageId)) return
+ const queued = state.messages.find(message => message.id === messageId)
if (queued === undefined || queued.targetId !== targetId) return
await this.journal.appendAndFlush(root, 'team/message/delivered', {
version: 1,
diff --git a/packages/experimental/agent-team/src/fold.ts b/packages/experimental/agent-team/src/projection.ts
similarity index 68%
rename from packages/experimental/agent-team/src/fold.ts
rename to packages/experimental/agent-team/src/projection.ts
index cf439a5322..0a873718e6 100644
--- a/packages/experimental/agent-team/src/fold.ts
+++ b/packages/experimental/agent-team/src/projection.ts
@@ -1,15 +1,15 @@
-/** Strict replay fold for Agent Teams log-only events. */
+/** Host-only Team state projected incrementally from committed Session events. */
import { z } from 'zod'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session'
+import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import type {
TeamId,
TeamMemberSnapshot,
TeamMessageId,
TeamMessageSnapshot,
- TeamTaskId,
TeamTaskSnapshot,
} from './types.ts'
import {
@@ -124,34 +124,53 @@ const teamMessageDeliveredEventSchema = z.object({
targetId: sessionIdSchema,
}).strict() as z.ZodType
-/** Mutable internal replay state. */
-export interface TeamFoldState {
+/** Current Team state selected by durable Team identity. */
+export interface TeamState {
readonly id: TeamId
- readonly members: Map
- readonly memberIdsByName: Map
- readonly tasks: Map
- readonly messages: Map
- readonly delivered: Set
+ readonly members: TeamMemberSnapshot[]
+ readonly tasks: TeamTaskSnapshot[]
+ readonly messages: TeamMessageSnapshot[]
+ readonly delivered: TeamMessageId[]
nextTaskNumber: number
}
/**
- * Construct an empty Team fold for one root Session.
- * @param rootId - Session whose TeamId selects applicable records.
- * @returns mutable empty replay state.
+ * Construct empty state for one Team identity.
+ * @param rootId - root Session identity.
+ * @returns mutable empty Team state.
*/
-export function emptyTeamFoldState(rootId: SessionId): TeamFoldState {
+export function emptyTeamState(rootId: SessionId): TeamProjectionState {
return {
id: toTeamId(rootId),
- members: new Map(),
- memberIdsByName: new Map(),
- tasks: new Map(),
- messages: new Map(),
- delivered: new Set(),
+ members: [],
+ tasks: [],
+ messages: [],
+ delivered: [],
nextTaskNumber: 1,
}
}
+/** Checkpoint-safe state for the Team owned by the projected Session. */
+export interface TeamProjectionState extends TeamState {
+ failure?: string
+}
+
+declare module '@deepseek-ai/dsh-session-projection/types' {
+ interface SessionProjectionStateMap {
+ agentTeam: TeamProjectionState
+ }
+}
+
+const teamProjectionEntrySchema = z.object({
+ id: teamIdSchema,
+ members: z.array(teamMemberSnapshotSchema),
+ tasks: z.array(teamTaskSnapshotSchema),
+ messages: z.array(teamMessageSnapshotSchema),
+ delivered: z.array(teamMessageIdSchema),
+ nextTaskNumber: positiveSafeInteger,
+ failure: z.string().optional(),
+}).strict() as z.ZodType
+
/** Whether one event belongs to the Team domain. */
export type TeamEventType =
| 'team/member'
@@ -160,7 +179,7 @@ export type TeamEventType =
| 'team/message/delivered'
/** One event owned by the Team domain. */
-export type TeamSessionEvent = SessionEvent
+type TeamSessionEvent = SessionEvent
/**
* Test whether a Session event belongs to the Team domain.
@@ -200,32 +219,34 @@ function parseCurrentTeamEvent(event: TeamSessionEvent): TeamSessionEvent {
}
}
-/**
- * Apply one event, ignoring Team records inherited by a different root fork.
- * @param state - mutable Team replay state.
- * @param event - next contiguous Session event.
- */
-export function applyTeamEvent(state: TeamFoldState, event: SessionEvent): void {
+function applyProjectionEvent(state: TeamProjectionState, event: SessionEvent): void {
+ if (state.failure !== undefined) return
if (!isTeamEvent(event)) return
- const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data)
- if (selector.version !== 1) {
+ try {
+ const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data)
if (selector.teamId !== state.id) return
- throw new Error(`unsupported Agent Teams event version ${String(selector.version)}`)
+ if (selector.version !== 1) {
+ throw new Error(`unsupported Agent Teams event version ${String(selector.version)}`)
+ }
+ applyCurrentTeamEvent(state, parseCurrentTeamEvent(event))
+ } catch (error: unknown) {
+ /* v8 ignore next -- the owned Team transition throws Error instances. */
+ state.failure = error instanceof Error ? error.message : String(error)
}
- const decoded = parseCurrentTeamEvent(event)
- if (decoded.data.teamId !== state.id) return
+}
- switch (decoded.type) {
+function applyCurrentTeamEvent(state: TeamState, event: TeamSessionEvent): void {
+ switch (event.type) {
case 'team/member': {
- const member = decoded.data.member
- const prior = state.members.get(member.id)
- const named = state.memberIdsByName.get(member.name)
- if (named !== undefined && named !== member.id) {
+ const member = event.data.member
+ const index = state.members.findIndex(candidate => candidate.id === member.id)
+ const prior = state.members[index]
+ const named = state.members.find(candidate => candidate.name === member.name)
+ if (named !== undefined && named.id !== member.id) {
throw new Error(`teammate name "${member.name}" is reused by another member`)
}
if (prior === undefined) {
if (member.phase !== 'provisioning') throw new Error(`teammate "${member.name}" must begin provisioning`)
- state.memberIdsByName.set(member.name, member.id)
} else {
if (prior.name !== member.name || prior.provider !== member.provider || prior.context !== member.context) {
throw new Error(`teammate "${member.id}" changed immutable identity fields`)
@@ -234,12 +255,14 @@ export function applyTeamEvent(state: TeamFoldState, event: SessionEvent): void
throw new Error(`teammate "${member.name}" has an invalid ${prior.phase} -> ${member.phase} transition`)
}
}
- state.members.set(member.id, member)
+ if (index < 0) state.members.push(member)
+ else state.members[index] = member
break
}
case 'team/task': {
- const task = decoded.data.task
- const prior = state.tasks.get(task.id)
+ const task = event.data.task
+ const index = state.tasks.findIndex(candidate => candidate.id === task.id)
+ const prior = state.tasks[index]
if (prior === undefined && task.revision !== 1) {
throw new Error(`team task "${task.id}" must begin at revision 1`)
}
@@ -255,21 +278,24 @@ export function applyTeamEvent(state: TeamFoldState, event: SessionEvent): void
number === Number.MAX_SAFE_INTEGER ? number : number + 1,
)
}
- state.tasks.set(task.id, task)
+ if (index < 0) state.tasks.push(task)
+ else state.tasks[index] = task
break
}
case 'team/message/queued': {
- const message = decoded.data.message
- if (state.messages.has(message.id)) throw new Error(`team message "${message.id}" was queued twice`)
- state.messages.set(message.id, message)
+ const message = event.data.message
+ if (state.messages.some(candidate => candidate.id === message.id)) {
+ throw new Error(`team message "${message.id}" was queued twice`)
+ }
+ state.messages.push(message)
break
}
case 'team/message/delivered': {
- const queued = state.messages.get(decoded.data.messageId)
- if (queued === undefined) throw new Error(`team message "${decoded.data.messageId}" was delivered before queueing`)
- if (queued.targetId !== decoded.data.targetId) throw new Error(`team message "${decoded.data.messageId}" target changed`)
- if (state.delivered.has(decoded.data.messageId)) throw new Error(`team message "${decoded.data.messageId}" was delivered twice`)
- state.delivered.add(decoded.data.messageId)
+ const queued = state.messages.find(message => message.id === event.data.messageId)
+ if (queued === undefined) throw new Error(`team message "${event.data.messageId}" was delivered before queueing`)
+ if (queued.targetId !== event.data.targetId) throw new Error(`team message "${event.data.messageId}" target changed`)
+ if (state.delivered.includes(event.data.messageId)) throw new Error(`team message "${event.data.messageId}" was delivered twice`)
+ state.delivered.push(event.data.messageId)
break
}
/* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
@@ -278,14 +304,14 @@ export function applyTeamEvent(state: TeamFoldState, event: SessionEvent): void
}
}
-/**
- * Replay one root Session into its current Team state.
- * @param rootId - root Session identity selecting Team-owned records.
- * @param events - complete contiguous Session log.
- * @returns mutable replay state at the end of the log.
- */
-export function foldTeam(rootId: SessionId, events: readonly SessionEvent[]): TeamFoldState {
- const state = emptyTeamFoldState(rootId)
- for (const event of events) applyTeamEvent(state, event)
- return state
-}
+/** Host-only Team projection selected by the projected Session identity. */
+export const teamProjectionDefinition = {
+ key: 'agentTeam',
+ stateVersion: 2,
+ stateSchema: teamProjectionEntrySchema,
+ init: header => emptyTeamState(header.id),
+ apply: (state, event) => {
+ applyProjectionEvent(state, event)
+ return state
+ },
+} satisfies ProjectionDefinition<'agentTeam', TeamProjectionState>
diff --git a/packages/experimental/agent-team/src/roster.ts b/packages/experimental/agent-team/src/roster.ts
index 2d8d190fce..907df82daa 100644
--- a/packages/experimental/agent-team/src/roster.ts
+++ b/packages/experimental/agent-team/src/roster.ts
@@ -8,9 +8,9 @@ import { SessionId } from '@deepseek-ai/dsh-session'
import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import type { ContinuableStart } from '@deepseek-ai/dsh-subagent'
import { errorMessage, TeamError } from './error.ts'
-import type { TeamFoldState } from './fold.ts'
import type { TeamJournal } from './journal.ts'
import type { TeamRuntimeLifecycle } from './lifecycle.ts'
+import type { TeamState } from './projection.ts'
import { messageAccepted } from './session-message.ts'
import { TeamId } from './types.ts'
import type {
@@ -34,19 +34,18 @@ export interface TeamMembership {
/**
* Resolve one active Team member by model-facing name, including the Lead pseudo-row.
* @param root - exact live Team Lead.
- * @param state - current Team fold.
+ * @param state - current Team state.
* @param rawName - candidate member name.
* @returns resolved durable id and normalized name.
*/
export function resolveActiveMember(
root: Agent,
- state: TeamFoldState,
+ state: TeamState,
rawName: string,
): { id: SessionId; name: string } {
const name = rawName.trim()
if (name === 'lead') return { id: root.id, name }
- const id = state.memberIdsByName.get(name)
- const member = id === undefined ? undefined : state.members.get(id)
+ const member = state.members.find(candidate => candidate.name === name)
if (member === undefined || member.phase !== 'active') {
throw new TeamError(`active teammate "${name}" not found`, 'TEAM_MEMBER_NOT_FOUND')
}
@@ -95,7 +94,7 @@ export class TeamRoster {
if (parentId !== undefined) {
const root = this.ctx.agents.get(parentId)
if (root !== undefined) {
- const member = this.journal.state(root).members.get(agent.id)
+ const member = this.journal.state(root).members.find(candidate => candidate.id === agent.id)
if (member?.phase === 'active' || member?.phase === 'provisioning') {
return { root, id: TeamId(root.id), role: 'teammate', name: member.name }
}
@@ -109,7 +108,7 @@ export class TeamRoster {
// A continuation can briefly outlive its parent during child-first teardown.
// Do not reinterpret that durable child as a new implicit root Team. A host-
// resumed ordinary fork has no descriptor in its own suffix and remains a
- // valid new root whose inherited Team records fold out by TeamId.
+ // valid new root whose inherited Team records stay outside its projected Team state.
if (this.subagentDescriptor(agent)) return undefined
return { root: agent, id: TeamId(agent.id), role: 'lead', name: 'lead' }
} catch {
@@ -136,7 +135,7 @@ export class TeamRoster {
...root.options.model === undefined ? {} : { model: root.options.model },
diagnostics: [],
}]
- for (const member of state.members.values()) {
+ for (const member of state.members) {
const live = this.ctx.agents.get(member.id)
const model = live?.options.model ?? root.options.model
result.push({
@@ -223,7 +222,8 @@ export class TeamRoster {
const rootId = agent.session.header.parentSession
if (rootId === undefined) continue
const root = this.ctx.agents.get(rootId)
- if (root === undefined || !this.journal.state(root).members.has(agent.id)) continue
+ if (root === undefined
+ || !this.journal.state(root).members.some(member => member.id === agent.id)) continue
const children = teams.get(root) ?? []
children.push(agent.id)
teams.set(root, children)
@@ -266,10 +266,10 @@ export class TeamRoster {
await this.journal.transact(root.id, async () => {
const state = this.journal.state(root)
- if (state.memberIdsByName.has(name)) {
+ if (state.members.some(member => member.name === name)) {
throw new TeamError(`teammate name "${name}" was already used in this Team`, 'TEAM_MEMBER_NAME_TAKEN')
}
- if (state.members.size >= this.maxMembers) {
+ if (state.members.length >= this.maxMembers) {
throw new TeamError(`Team member limit ${this.maxMembers} reached`, 'TEAM_MEMBER_LIMIT')
}
await this.journal.appendAndFlush(root, 'team/member', { version: 1, teamId: TeamId(root.id), member })
@@ -387,7 +387,7 @@ export class TeamRoster {
/** Settle provisioning-only members from their independently durable child Sessions. */
private async reconcileProvisioning(root: Agent, signal: AbortSignal): Promise {
- const provisioning = [...this.journal.state(root).members.values()].filter(member => member.phase === 'provisioning')
+ const provisioning = this.journal.state(root).members.filter(member => member.phase === 'provisioning')
for (const member of provisioning) {
signal.throwIfAborted()
// A live child means creation is still completing in this process. Its
@@ -414,7 +414,7 @@ export class TeamRoster {
signal.throwIfAborted()
await this.journal.transact(root.id, async () => {
signal.throwIfAborted()
- const current = this.journal.state(root).members.get(member.id)
+ const current = this.journal.state(root).members.find(candidate => candidate.id === member.id)
if (current?.phase !== 'provisioning') return
const settled: TeamMemberSnapshot = {
...current,
@@ -463,7 +463,7 @@ export class TeamRoster {
terminal: TeamMemberSnapshot,
): Promise<'active' | 'failed'> {
return this.journal.transact(root.id, async () => {
- const current = this.journal.state(root).members.get(terminal.id)
+ const current = this.journal.state(root).members.find(member => member.id === terminal.id)
/* v8 ignore next 3 -- the append-only provisioning event is committed by this operation before settlement. */
if (current === undefined) {
throw new TeamError(`provisioned teammate "${terminal.id}" disappeared`, 'TEAM_PROVISIONING_CONFLICT')
diff --git a/packages/experimental/agent-team/src/task-board.ts b/packages/experimental/agent-team/src/task-board.ts
index f987dd9217..5dd235f940 100644
--- a/packages/experimental/agent-team/src/task-board.ts
+++ b/packages/experimental/agent-team/src/task-board.ts
@@ -3,8 +3,8 @@
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { TeamMembership } from './roster.ts'
import { TeamError } from './error.ts'
-import type { TeamFoldState } from './fold.ts'
import type { TeamJournal } from './journal.ts'
+import type { TeamState } from './projection.ts'
import { resolveActiveMember } from './roster.ts'
import { assertTaskGraphCandidate, TeamTaskGraphError } from './task-graph.ts'
import type { TeamTaskGraphViolation } from './task-graph.ts'
@@ -49,12 +49,12 @@ export class TeamTaskBoard {
const { root } = membership
return this.journal.transact(root.id, async () => {
const state = this.journal.state(root)
- const active = [...state.tasks.values()].filter(task => task.status !== 'deleted').length
+ const active = state.tasks.filter(task => task.status !== 'deleted').length
if (active >= this.maxTasks) {
throw new TeamError(`Team task limit ${this.maxTasks} reached`, 'TEAM_TASK_LIMIT')
}
const id = TeamTaskId(`task-${state.nextTaskNumber}`)
- if (state.tasks.has(id)) {
+ if (state.tasks.some(task => task.id === id)) {
throw new TeamError('Team task id space exhausted', 'TEAM_TASK_LIMIT')
}
const task: TeamTaskSnapshot = {
@@ -81,7 +81,7 @@ export class TeamTaskBoard {
get(membership: TeamMembership, id: TeamTaskId): TeamTaskView {
const { root } = membership
const state = this.journal.state(root)
- const task = state.tasks.get(id)
+ const task = state.tasks.find(candidate => candidate.id === id)
if (task === undefined) throw new TeamError(`team task "${id}" not found`, 'TEAM_TASK_NOT_FOUND')
return this.taskView(root, state, task)
}
@@ -94,7 +94,7 @@ export class TeamTaskBoard {
list(membership: TeamMembership): TeamTaskView[] {
const { root } = membership
const state = this.journal.state(root)
- return [...state.tasks.values()]
+ return state.tasks
.filter(task => task.status !== 'deleted')
.map(task => this.taskView(root, state, task))
}
@@ -114,7 +114,7 @@ export class TeamTaskBoard {
const root = membership.root
return this.journal.transact(root.id, async () => {
const state = this.journal.state(root)
- const current = state.tasks.get(request.taskId)
+ const current = state.tasks.find(task => task.id === request.taskId)
if (current === undefined) throw new TeamError(`team task "${request.taskId}" not found`, 'TEAM_TASK_NOT_FOUND')
if (current.revision !== request.expectedRevision) {
throw new TeamError(
@@ -192,7 +192,7 @@ export class TeamTaskBoard {
}
case 'delete': {
authorizeOwner()
- const dependent = [...state.tasks.values()].find(task =>
+ const dependent = state.tasks.find(task =>
task.status !== 'deleted' && task.id !== current.id && task.blockedBy.includes(current.id))
if (dependent !== undefined) {
throw new TeamError(`team task "${current.id}" still blocks "${dependent.id}"`, 'TEAM_TASK_HAS_DEPENDENTS')
@@ -217,7 +217,7 @@ export class TeamTaskBoard {
/** Validate and de-duplicate dependency ids against the current task graph. */
private dependencies(
values: readonly TeamTaskId[],
- state: TeamFoldState,
+ state: TeamState,
self?: TeamTaskId,
): TeamTaskId[] {
const seen = new Set()
@@ -225,7 +225,7 @@ export class TeamTaskBoard {
for (const id of values) {
if (id === self) throw new TeamError('a team task cannot block itself', 'TEAM_TASK_DEPENDENCY_CYCLE')
if (seen.has(id)) throw new TeamError(`duplicate blocker "${id}"`, 'TEAM_INVALID_ARGUMENT')
- const task = state.tasks.get(id)
+ const task = state.tasks.find(candidate => candidate.id === id)
if (task === undefined || task.status === 'deleted') {
throw new TeamError(`blocker task "${id}" not found`, 'TEAM_TASK_NOT_FOUND')
}
@@ -241,7 +241,7 @@ export class TeamTaskBoard {
}
/** Map shared task-graph validation onto stable command error codes. */
- private assertTaskGraph(state: TeamFoldState, candidate: TeamTaskSnapshot): void {
+ private assertTaskGraph(state: TeamState, candidate: TeamTaskSnapshot): void {
try {
assertTaskGraphCandidate(state.tasks, candidate)
} catch (error: unknown) {
@@ -252,8 +252,8 @@ export class TeamTaskBoard {
}
/** Whether all current blockers completed. */
- private taskReady(state: TeamFoldState, task: TeamTaskSnapshot): boolean {
- return task.blockedBy.every(id => state.tasks.get(id)?.status === 'completed')
+ private taskReady(state: TeamState, task: TeamTaskSnapshot): boolean {
+ return task.blockedBy.every(id => state.tasks.find(candidate => candidate.id === id)?.status === 'completed')
}
/** Remove an optional owner field under exactOptionalPropertyTypes. */
@@ -264,18 +264,18 @@ export class TeamTaskBoard {
/**
* Build one task view with owner name, readiness, and advisory write overlaps.
- * A committing caller may pass its pre-append fold because `task` supplies the
+ * A committing caller may pass its pre-append state because `task` supplies the
* new value explicitly; owner names, blocker readiness, and other task scopes
* do not change when that snapshot is appended.
*/
- private taskView(root: Agent, state: TeamFoldState, task: TeamTaskSnapshot): TeamTaskView {
+ private taskView(root: Agent, state: TeamState, task: TeamTaskSnapshot): TeamTaskView {
const ownerName = task.ownerId === undefined
? undefined
: task.ownerId === root.id
? 'lead'
- : state.members.get(task.ownerId)?.name
+ : state.members.find(member => member.id === task.ownerId)?.name
const warnings = new Set()
- for (const other of state.tasks.values()) {
+ for (const other of state.tasks) {
if (other.id === task.id || other.status !== 'in_progress') continue
if (task.writeScopes.some(left => other.writeScopes.some(right => scopesOverlap(left, right)))) {
warnings.add(`write scopes overlap with ${other.id}`)
diff --git a/packages/experimental/agent-team/src/task-graph.ts b/packages/experimental/agent-team/src/task-graph.ts
index 38d9dd144c..bd61b953bc 100644
--- a/packages/experimental/agent-team/src/task-graph.ts
+++ b/packages/experimental/agent-team/src/task-graph.ts
@@ -24,10 +24,10 @@ export class TeamTaskGraphError extends Error {
* @throws {TeamTaskGraphError} when an active dependency is missing, duplicated, self-referential, or cyclic.
*/
export function assertTaskGraphCandidate(
- current: ReadonlyMap,
+ current: readonly TeamTaskSnapshot[],
candidate: TeamTaskSnapshot,
): void {
- const tasks = new Map(current)
+ const tasks = new Map(current.map(task => [task.id, task]))
tasks.set(candidate.id, candidate)
for (const task of tasks.values()) {
diff --git a/packages/experimental/agent-team/tests/invariant.spec.ts b/packages/experimental/agent-team/tests/invariant.spec.ts
index 3993070f26..ba12a07c74 100644
--- a/packages/experimental/agent-team/tests/invariant.spec.ts
+++ b/packages/experimental/agent-team/tests/invariant.spec.ts
@@ -2,12 +2,16 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import InvariantService, { InvariantError } from '@deepseek-ai/dsh-invariants'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as TeamInvariant from '../src/invariant.ts'
+import { teamProjectionDefinition } from '../src/projection.ts'
import { TeamId, TeamTaskId } from '../src/types.ts'
async function setup(): Promise {
const ctx = new Context()
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
+ ctx.sessionProjections.register(teamProjectionDefinition)
await ctx.plugin(InvariantService, { enabled: true })
await ctx.plugin(TeamInvariant)
return ctx
diff --git a/packages/experimental/agent-team/tests/persistence.spec.ts b/packages/experimental/agent-team/tests/persistence.spec.ts
index 46020874ca..88e72e5506 100644
--- a/packages/experimental/agent-team/tests/persistence.spec.ts
+++ b/packages/experimental/agent-team/tests/persistence.spec.ts
@@ -9,12 +9,14 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SqliteSessionPersistence from '@deepseek-ai/dsh-session-persistence-sqlite'
import SubagentService, { seedDescriptorTurn, snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
-import TeamService, { foldTeam, TeamId, TeamMessageId } from '../src/index.ts'
+import TeamService, { TeamId, TeamMessageId } from '../src/index.ts'
+import { teamProjectionDefinition } from '../src/projection.ts'
import type { TeamMemberSnapshot, TeamMessageSnapshot, TeamTaskSnapshot } from '../src/index.ts'
import { TestSessionQuery } from './test-session-query.ts'
@@ -23,17 +25,20 @@ const PERSISTENCE_TEST_TIMEOUT_MS = 15_000
const roots: string[] = []
const contexts = new Set()
-/** Detached durable Team read: the service exposes views, so assertions fold the Lead log. */
+/** Detached durable Team read through the same projection definition as the service. */
function durable(agent: Agent): {
members: TeamMemberSnapshot[]
tasks: TeamTaskSnapshot[]
pendingMessages: TeamMessageSnapshot[]
} {
- const state = foldTeam(agent.id, agent.session.events)
+ let projected = teamProjectionDefinition.init(agent.session.header)
+ for (const event of agent.session.events) projected = teamProjectionDefinition.apply(projected, event)
+ if (projected.failure !== undefined) throw new Error(projected.failure)
+ const state = projected
return {
- members: [...state.members.values()],
- tasks: [...state.tasks.values()],
- pendingMessages: [...state.messages.values()].filter(message => !state.delivered.has(message.id)),
+ members: state.members,
+ tasks: state.tasks,
+ pendingMessages: state.messages.filter(message => !state.delivered.includes(message.id)),
}
}
@@ -94,6 +99,7 @@ async function stack(
const ctx = new Context()
contexts.add(ctx)
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await backend.mount(ctx, root)
await ctx.plugin(TestSessionQuery)
await ctx.plugin(AgentLoop, { agents: [] })
diff --git a/packages/experimental/agent-team/tests/fold.spec.ts b/packages/experimental/agent-team/tests/projection-events.spec.ts
similarity index 70%
rename from packages/experimental/agent-team/tests/fold.spec.ts
rename to packages/experimental/agent-team/tests/projection-events.spec.ts
index d1ed095a99..3cd6b6103c 100644
--- a/packages/experimental/agent-team/tests/fold.spec.ts
+++ b/packages/experimental/agent-team/tests/projection-events.spec.ts
@@ -1,13 +1,8 @@
import { describe, expect, it } from 'vitest'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionEventMap, SessionEventType } from '@deepseek-ai/dsh-session'
-import {
- applyTeamEvent,
- emptyTeamFoldState,
- foldTeam,
- isTeamEvent,
-} from '../src/fold.ts'
-import type { TeamFoldState } from '../src/fold.ts'
+import { teamProjectionDefinition } from '../src/projection.ts'
+import type { TeamProjectionState, TeamState } from '../src/projection.ts'
import { TeamId, TeamMessageId, TeamTaskId } from '../src/types.ts'
import type { TeamMemberSnapshot, TeamMessageSnapshot, TeamTaskSnapshot } from '../src/types.ts'
@@ -19,15 +14,30 @@ function event(type: T, data: SessionEventMap[T], se
return { type, data, seq, time: seq } as SessionEvent
}
-/** Queued-minus-delivered mail, the recovery mailbox the fold is responsible for. */
-function pending(state: TeamFoldState): TeamMessageSnapshot[] {
- return [...state.messages.values()].filter(message => !state.delivered.has(message.id))
+function project(rootId: SessionId, events: readonly SessionEvent[]): TeamProjectionState {
+ let state = teamProjectionDefinition.init({ version: 0, id: rootId, createdAt: 0 })
+ for (const event of events) state = teamProjectionDefinition.apply(state, event)
+ return state
}
-/** Whether one fold reached the end of its log without applying any Team record. */
-function isEmptyFold(state: TeamFoldState): boolean {
- return state.members.size === 0 && state.tasks.size === 0
- && state.messages.size === 0 && state.delivered.size === 0
+function teamState(projected: TeamProjectionState): TeamState {
+ if (projected.failure !== undefined) throw new Error(projected.failure)
+ return projected
+}
+
+function projectTeam(rootId: SessionId, events: readonly SessionEvent[]): TeamState {
+ return teamState(project(rootId, events))
+}
+
+/** Queued-minus-delivered mail retained by the projection. */
+function pending(state: TeamState): TeamMessageSnapshot[] {
+ return state.messages.filter(message => !state.delivered.includes(message.id))
+}
+
+/** Whether one Team state contains no projected records. */
+function isEmptyState(state: TeamState): boolean {
+ return state.members.length === 0 && state.tasks.length === 0
+ && state.messages.length === 0 && state.delivered.length === 0
}
function member(overrides: Partial = {}): TeamMemberSnapshot {
@@ -67,8 +77,8 @@ function message(overrides: Partial = {}): TeamMessageSnaps
}
}
-describe('Agent Teams fold', () => {
- it('folds current-team records and ignores inherited records', () => {
+describe('Agent Teams projection events', () => {
+ it('projects current-team records independently from inherited records', () => {
const records: SessionEvent[] = [
event('team/member', { version: 1, teamId: TeamId('ancestor'), member: member() }, 0),
event('team/member', { version: 1, teamId: TEAM, member: member() }, 1),
@@ -80,31 +90,32 @@ describe('Agent Teams fold', () => {
event('team/task', { version: 1, teamId: TEAM, task: task({ id: TeamTaskId('task-7') }) }, 3),
event('team/message/queued', { version: 1, teamId: TEAM, message: message() }, 4),
]
- const state = foldTeam(ROOT, records)
+ const projected = project(ROOT, records)
+ const state = teamState(projected)
expect(state).toMatchObject({ id: TEAM })
- expect(state.members.size).toBe(1)
- expect(state.tasks.size).toBe(1)
+ expect(state.members).toHaveLength(1)
+ expect(state.tasks).toHaveLength(1)
expect(pending(state)).toHaveLength(1)
expect(state.nextTaskNumber).toBe(8)
- expect(state.members.get(CHILD)?.name).toBe('worker-a')
- expect(isTeamEvent(records[0]!)).toBe(true)
- expect(isTeamEvent(event('turn/start', { turn: 1 }, 5))).toBe(false)
+ expect(state.members.find(member => member.id === CHILD)?.name).toBe('worker-a')
+ expect(teamProjectionDefinition.stateSchema.parse(JSON.parse(JSON.stringify(projected))))
+ .toEqual(projected)
})
it('enforces teammate identity and lifecycle', () => {
const base = event('team/member', { version: 1, teamId: TEAM, member: member() }, 0)
- expect(() => foldTeam(ROOT, [event('team/member', {
+ expect(() => projectTeam(ROOT, [event('team/member', {
version: 1,
teamId: TEAM,
member: member({ phase: 'active' }),
}, 0)])).toThrow(/must begin provisioning/)
- expect(() => foldTeam(ROOT, [base, event('team/member', {
+ expect(() => projectTeam(ROOT, [base, event('team/member', {
version: 1,
teamId: TEAM,
member: member({ name: 'renamed', phase: 'active' }),
}, 1)])).toThrow(/immutable identity/)
- expect(() => foldTeam(ROOT, [base, event('team/member', {
+ expect(() => projectTeam(ROOT, [base, event('team/member', {
version: 1,
teamId: TEAM,
member: member({ phase: 'active' }),
@@ -115,7 +126,7 @@ describe('Agent Teams fold', () => {
}, 2)])).toThrow(/invalid active -> failed/)
const duplicateName = member({ id: SessionId('child-b') })
- expect(() => foldTeam(ROOT, [base, event('team/member', {
+ expect(() => projectTeam(ROOT, [base, event('team/member', {
version: 1,
teamId: TEAM,
member: duplicateName,
@@ -124,12 +135,12 @@ describe('Agent Teams fold', () => {
it('enforces task revision continuity', () => {
const first = event('team/task', { version: 1, teamId: TEAM, task: task() }, 0)
- expect(() => foldTeam(ROOT, [event('team/task', {
+ expect(() => projectTeam(ROOT, [event('team/task', {
version: 1,
teamId: TEAM,
task: task({ revision: 2 }),
}, 0)])).toThrow(/begin at revision 1/)
- expect(() => foldTeam(ROOT, [first, event('team/task', {
+ expect(() => projectTeam(ROOT, [first, event('team/task', {
version: 1,
teamId: TEAM,
task: task({ revision: 3 }),
@@ -189,12 +200,12 @@ describe('Agent Teams fold', () => {
]
for (const { records, message: expected } of invalid) {
- expect(() => foldTeam(ROOT, records)).toThrow(expected)
+ expect(() => projectTeam(ROOT, records)).toThrow(expected)
}
})
it('leaves numeric allocation unchanged for a branded nonstandard task id', () => {
- const state = foldTeam(ROOT, [event('team/task', {
+ const state = projectTeam(ROOT, [event('team/task', {
version: 1,
teamId: TEAM,
task: task({ id: TeamTaskId('external-task') }),
@@ -203,7 +214,7 @@ describe('Agent Teams fold', () => {
})
it('rejects a persisted numeric task id outside the safe integer range', () => {
- expect(() => foldTeam(ROOT, [event('team/task', {
+ expect(() => projectTeam(ROOT, [event('team/task', {
version: 1,
teamId: TEAM,
task: task({ id: TeamTaskId('task-9007199254740992') }),
@@ -218,17 +229,17 @@ describe('Agent Teams fold', () => {
messageId: TeamMessageId('message-1'),
targetId: CHILD,
}, 1)
- expect(pending(foldTeam(ROOT, [queued, delivered]))).toEqual([])
- expect(() => foldTeam(ROOT, [queued, queued])).toThrow(/queued twice/)
- expect(() => foldTeam(ROOT, [delivered])).toThrow(/delivered before queueing/)
- expect(() => foldTeam(ROOT, [queued, event('team/message/delivered', {
+ expect(pending(projectTeam(ROOT, [queued, delivered]))).toEqual([])
+ expect(() => projectTeam(ROOT, [queued, queued])).toThrow(/queued twice/)
+ expect(() => projectTeam(ROOT, [delivered])).toThrow(/delivered before queueing/)
+ expect(() => projectTeam(ROOT, [queued, event('team/message/delivered', {
...delivered.data,
targetId: SessionId('other'),
}, 1)])).toThrow(/target changed/)
- expect(() => foldTeam(ROOT, [queued, delivered, { ...delivered, seq: 2 }])).toThrow(/delivered twice/)
+ expect(() => projectTeam(ROOT, [queued, delivered, { ...delivered, seq: 2 }])).toThrow(/delivered twice/)
})
- it('validates every current-version persisted payload before folding it', () => {
+ it('validates every current-version persisted payload before projecting it', () => {
const malformed = [
{
...event('team/member', { version: 1, teamId: TEAM, member: member() }, 0),
@@ -271,14 +282,14 @@ describe('Agent Teams fold', () => {
] as unknown as SessionEvent[]
for (const candidate of malformed) {
- expect(() => foldTeam(ROOT, [candidate]))
+ expect(() => projectTeam(ROOT, [candidate]))
.toThrow(/persisted Agent Teams .* payload is invalid/)
}
})
it('retains merge-extensible content blocks while rejecting malformed core variants', () => {
const extension = { type: 'plugin/custom', payload: { value: 1 } } as never
- const state = foldTeam(ROOT, [event('team/message/queued', {
+ const state = projectTeam(ROOT, [event('team/message/queued', {
version: 1,
teamId: TEAM,
message: message({ content: [extension] }),
@@ -286,27 +297,34 @@ describe('Agent Teams fold', () => {
expect(pending(state)[0]?.content).toEqual([extension])
})
- it('rejects unsupported event versions without mutating an empty state', () => {
- const state = emptyTeamFoldState(ROOT)
+ it('records unsupported event versions without applying them', () => {
const invalid = event('team/task', {
version: 2 as 1,
teamId: TEAM,
task: task(),
}, 0)
- expect(() => { applyTeamEvent(state, invalid) }).toThrow(/unsupported Agent Teams event version 2/)
- expect(isEmptyFold(state)).toBe(true)
+ const later = event('team/task', {
+ version: 1,
+ teamId: TEAM,
+ task: task(),
+ }, 1)
+ const state = project(ROOT, [invalid, later])
+ expect(state.failure).toMatch(/unsupported Agent Teams event version 2/)
+ expect(isEmptyState(state)).toBe(true)
})
- it('ignores unsupported inherited Team records before decoding their version', () => {
+ it('isolates unsupported inherited Team records from the current Team', () => {
const inherited = event('team/task', {
version: 2 as 1,
teamId: TeamId('ancestor'),
task: task(),
}, 0)
- expect(isEmptyFold(foldTeam(ROOT, [inherited]))).toBe(true)
+ const projected = project(ROOT, [inherited])
+ expect(projected.failure).toBeUndefined()
+ expect(isEmptyState(teamState(projected))).toBe(true)
})
- it('still validates complete current-version records inherited from another Team', () => {
+ it('ignores malformed current-version records inherited from another Team', () => {
const inherited = {
...event('team/task', {
version: 1,
@@ -319,7 +337,6 @@ describe('Agent Teams fold', () => {
task: { ...task(), subject: 42 },
},
} as unknown as SessionEvent
- expect(() => foldTeam(ROOT, [inherited]))
- .toThrow(/persisted Agent Teams team\/task payload is invalid/)
+ expect(isEmptyState(projectTeam(ROOT, [inherited]))).toBe(true)
})
})
diff --git a/packages/experimental/agent-team/tests/team.spec.ts b/packages/experimental/agent-team/tests/team.spec.ts
index d5c471d665..21d0e61b4a 100644
--- a/packages/experimental/agent-team/tests/team.spec.ts
+++ b/packages/experimental/agent-team/tests/team.spec.ts
@@ -8,13 +8,15 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId, type Session } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
-import TeamService, { foldTeam, TeamError, TeamId, TeamMessageId, TeamTaskId } from '../src/index.ts'
+import TeamService, { TeamError, TeamId, TeamMessageId, TeamTaskId } from '../src/index.ts'
import { TeamRuntimeLifecycle } from '../src/lifecycle.ts'
+import { teamProjectionDefinition } from '../src/projection.ts'
import type { TeamMemberSnapshot, TeamMessageSnapshot, TeamTaskSnapshot } from '../src/index.ts'
import { TestSessionQuery } from './test-session-query.ts'
@@ -26,17 +28,20 @@ afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
-/** Detached durable Team read: the service exposes views, so assertions fold the Lead log. */
+/** Detached durable Team read through the same projection definition as the service. */
function durable(agent: Agent): {
members: TeamMemberSnapshot[]
tasks: TeamTaskSnapshot[]
pendingMessages: TeamMessageSnapshot[]
} {
- const state = foldTeam(agent.id, agent.session.events)
+ let projected = teamProjectionDefinition.init(agent.session.header)
+ for (const event of agent.session.events) projected = teamProjectionDefinition.apply(projected, event)
+ if (projected.failure !== undefined) throw new Error(projected.failure)
+ const state = projected
return {
- members: [...state.members.values()],
- tasks: [...state.tasks.values()],
- pendingMessages: [...state.messages.values()].filter(message => !state.delivered.has(message.id)),
+ members: state.members,
+ tasks: state.tasks,
+ pendingMessages: state.messages.filter(message => !state.delivered.includes(message.id)),
}
}
@@ -46,6 +51,7 @@ async function setup(
) {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
const storageRoot = mkdtempSync(join(tmpdir(), 'dsh-team-'))
roots.push(storageRoot)
await ctx.plugin(JsonlSessionPersistence, { root: storageRoot })
@@ -120,6 +126,21 @@ async function waitRunning(ctx: Context, id: SessionId): Promise {
}
describe('Team identity and provisioning', () => {
+ it('rejects missing and failed authoritative Team projections', async () => {
+ const first = await setup([])
+ const journal = teamInternals(first.ctx).journal
+ const stateOf = first.ctx.sessionProjections.stateOf.bind(first.ctx.sessionProjections)
+ const stateOfSpy = vi.spyOn(first.ctx.sessionProjections, 'stateOf').mockImplementation((session, key) => (
+ key === 'agentTeam' ? undefined : stateOf(session, key)
+ ))
+ expect(() => journal.state(first.lead)).toThrow('Agent Teams projection is not registered')
+ stateOfSpy.mockImplementation((session, key) => key === 'agentTeam'
+ ? { ...teamProjectionDefinition.init(session.header), failure: 'failed Team projection' }
+ : stateOf(session, key))
+ expect(() => journal.state(first.lead)).toThrow('failed Team projection')
+ stateOfSpy.mockRestore()
+ })
+
it('rejects deployment limits that are not positive safe integers', async () => {
const fields = [
'maxMembers',
@@ -138,6 +159,7 @@ describe('Team identity and provisioning', () => {
it('supports direct-constructor defaults and recovers roots that already exist', async () => {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
const storageRoot = mkdtempSync(join(tmpdir(), 'dsh-team-direct-'))
roots.push(storageRoot)
await ctx.plugin(JsonlSessionPersistence, { root: storageRoot })
@@ -1331,6 +1353,7 @@ describe('Team mailbox and waiting', () => {
it('waits for one change, supports cancellation, times out, and releases waiters on HMR disposal', async () => {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
const storageRoot = mkdtempSync(join(tmpdir(), 'dsh-team-wait-'))
roots.push(storageRoot)
await ctx.plugin(JsonlSessionPersistence, { root: storageRoot })
diff --git a/packages/experimental/agent-team/tsconfig.json b/packages/experimental/agent-team/tsconfig.json
index 22d7ade886..862b4b5836 100644
--- a/packages/experimental/agent-team/tsconfig.json
+++ b/packages/experimental/agent-team/tsconfig.json
@@ -16,6 +16,7 @@
{ "path": "../../typert/protocol" },
{ "path": "../../subagent/subagent" },
{ "path": "../../session/session-persistence" },
+ { "path": "../../session/session-projection" },
{ "path": "../../runtime-diagnostics/invariants" }
]
}
diff --git a/packages/experimental/agent-team/tsdown.config.ts b/packages/experimental/agent-team/tsdown.config.ts
index e630b728b4..cc38b29b83 100644
--- a/packages/experimental/agent-team/tsdown.config.ts
+++ b/packages/experimental/agent-team/tsdown.config.ts
@@ -1,6 +1,6 @@
import { defineConfig } from 'tsdown'
-/** Build the runtime and invariant as independent bundles so shared fold code stays package-local. */
+/** Build the runtime and invariant companion as independent package exports. */
export default defineConfig([
{
entry: ['lib/types/index.js'],
diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json
index dccaf91caa..c21cd7a6c2 100644
--- a/packages/experimental/client-ui-agent-team/package.json
+++ b/packages/experimental/client-ui-agent-team/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-experimental-client-ui-agent-team",
"description": "Web Agent Teams roster, task board, and teammate navigation",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"private": true,
"repository": {
"type": "git",
diff --git a/packages/experimental/inspector/package.json b/packages/experimental/inspector/package.json
index c13ed177d2..34bc8c9a1c 100644
--- a/packages/experimental/inspector/package.json
+++ b/packages/experimental/inspector/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-experimental-inspector",
"description": "Experimental cross-realm CDP hub for Host debugging and Client Runtime inspection",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"private": true,
"repository": {
"type": "git",
diff --git a/packages/experimental/tool-agent-team/package.json b/packages/experimental/tool-agent-team/package.json
index aae5baad20..f04bb5d9c6 100644
--- a/packages/experimental/tool-agent-team/package.json
+++ b/packages/experimental/tool-agent-team/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-experimental-tool-agent-team",
"description": "Scoped model-facing Agent Teams tools over ctx.agentTeams",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"private": true,
"repository": {
"type": "git",
@@ -47,6 +47,7 @@
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-session-query": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
diff --git a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts
index 1efc795932..db80af0ed2 100644
--- a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts
+++ b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts
@@ -9,6 +9,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import { scopeOf } from '@deepseek-ai/dsh-scope'
import { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
import SubagentService from '@deepseek-ai/dsh-subagent'
@@ -56,6 +57,7 @@ afterEach(() => {
async function setup(script: ConstructorParameters[0], legacyControl = false) {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
const storageRoot = mkdtempSync(join(tmpdir(), 'dsh-tool-team-'))
roots.push(storageRoot)
await ctx.plugin(JsonlSessionPersistence, { root: storageRoot })
diff --git a/packages/experimental/webworker-packer/package.json b/packages/experimental/webworker-packer/package.json
index a8155676dd..9fa51d3fe3 100644
--- a/packages/experimental/webworker-packer/package.json
+++ b/packages/experimental/webworker-packer/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-experimental-webworker-packer",
"description": "Build-time packer for the browser runtime's base VFS image and ordered data-overlay archives",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"private": true,
"repository": {
"type": "git",
diff --git a/packages/experimental/webworker-runtime/package.json b/packages/experimental/webworker-runtime/package.json
index bfcff40db5..313db801e2 100644
--- a/packages/experimental/webworker-runtime/package.json
+++ b/packages/experimental/webworker-runtime/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-experimental-webworker-runtime",
"description": "Browser-only harness runtime: in-memory VFS, module transform and loader, postMessage tunnel, and the dedicated Web Worker assembly, with the Node-compatibility layer that lets the host tree run unchanged",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"private": true,
"repository": {
"type": "git",
@@ -58,6 +58,7 @@
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
diff --git a/packages/experimental/webworker-runtime/tests/node/sandbox-stack.spec.ts b/packages/experimental/webworker-runtime/tests/node/sandbox-stack.spec.ts
index 599608d598..1194f50da0 100644
--- a/packages/experimental/webworker-runtime/tests/node/sandbox-stack.spec.ts
+++ b/packages/experimental/webworker-runtime/tests/node/sandbox-stack.spec.ts
@@ -4,6 +4,7 @@ import { Context } from '@deepseek-ai/cordis'
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
import LocalSandboxProvider from '@deepseek-ai/dsh-sandbox-local'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { MemoryVfs } from '../../src/storage/memory.ts'
import { setActiveVfs } from '../../src/storage/active.ts'
@@ -44,6 +45,7 @@ async function setup(mode: 'read-only' | 'workspace-write' | 'danger-full-access
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(LocalSandboxProvider)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: WORKSPACE })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(SandboxBashExecutor, { cwd: WORKSPACE })
diff --git a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts
index 561cf04699..d4d1471d0d 100644
--- a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts
+++ b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts
@@ -62,7 +62,12 @@ describe('WebWorker preview VFS example', () => {
expect(cache.unit).toEqual({ name: 'session_projcache', version: 3 })
expect(cache.tables.sessions[VFS_EXAMPLE_SESSION_IDS.main]).toMatchObject({
identity: { createdAt: 1_787_472_000_000, cwd: '/dsh/workspace' },
- rows: { title: { ver: 1, val: VFS_EXAMPLE_TITLE } },
+ rows: {
+ title: {
+ ver: 1,
+ val: VFS_EXAMPLE_TITLE,
+ },
+ },
})
})
diff --git a/packages/extensions/cordis-client-runner/package.json b/packages/extensions/cordis-client-runner/package.json
index 3bc0539e38..6c3e49abed 100644
--- a/packages/extensions/cordis-client-runner/package.json
+++ b/packages/extensions/cordis-client-runner/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-cordis-client-runner",
"description": "Browser half of dynamic dual-half plugin packages: event subscription, closure evaluation, guard facade, and loader entries",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/extensions/cordis-host-runner/README.i18n.yaml b/packages/extensions/cordis-host-runner/README.i18n.yaml
index 12a5c13bb9..266bc35a25 100644
--- a/packages/extensions/cordis-host-runner/README.i18n.yaml
+++ b/packages/extensions/cordis-host-runner/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/extensions/cordis-host-runner/README.md
-README.md: 0efd3afcff94027408f732541d3d2b4d3470843d
-README.zh.md: a92a4c281ded7395756c5e8f8423e8e61a4d1af2
+README.md: ec229d225955718977679b00db522ddcf4a3035e
+README.zh.md: 862746798e355d9f5c53fad2b8c69be4d73db3eb
diff --git a/packages/extensions/cordis-host-runner/README.md b/packages/extensions/cordis-host-runner/README.md
index 0efd3afcff..ec229d2259 100644
--- a/packages/extensions/cordis-host-runner/README.md
+++ b/packages/extensions/cordis-host-runner/README.md
@@ -131,7 +131,7 @@ These limits define when the runner needs special care. They are current package
- **`vmTimeoutMs` bounds only synchronous evaluation** — an async host-half body escapes it, matching the toolset's cooperative trust stance.
- **A stale-success refusal leaves the request suspended** — when the answering page names a revision the registry has moved past, the resolution is refused (`accepted: false`) and the request stays answerable until another page answers or the caller cancels; the browser half does not read the acknowledgement.
- **The run announcement carries no service declarations** — a browser half's declared `inject` is read from the plugin it returns in the page, so `cordis/request-run` carries metadata only, never code or service lists.
-- **`zod` is a runtime dependency of the generated Typert faces, not of `src`** — `./typert` and `./remote` resolve to unbundled `lib` files with a bare `import { z } from 'zod'`, so the package declares it and `knip.json` ignores it for this workspace; nothing in `src` imports zod.
+- **`zod` is a runtime dependency of the generated Typert faces, not of `src`** — `./typert` and `./remote` resolve to unbundled `lib` files with a bare `import { z } from 'zod'`, so the package declares it even though nothing in `src` imports zod.
### Dev Note
diff --git a/packages/extensions/cordis-host-runner/README.zh.md b/packages/extensions/cordis-host-runner/README.zh.md
index a92a4c281d..862746798e 100644
--- a/packages/extensions/cordis-host-runner/README.zh.md
+++ b/packages/extensions/cordis-host-runner/README.zh.md
@@ -131,7 +131,7 @@ runner 建立在两个分离之上。**注册表与沙箱是同一个服务。**
- **`vmTimeoutMs` 只约束同步求值**——async 的 host 半函数体会逃出该上限,这与工具集基于协作的信任立场一致。
- **陈旧成功的拒绝会让请求继续挂起**——作答页面点名的 revision 已被注册表越过时,该结论会被拒绝(`accepted: false`),请求保持可作答,直到另一个页面作答或调用方取消;浏览器半不读这个 ack。
- **运行播报不携带服务声明**——浏览器半声明的 `inject` 是从它在页面里返回的插件上读出的,因此 `cordis/request-run` 只携带元数据,绝无代码或服务清单。
-- **`zod` 是生成的 Typert 契约面的运行时依赖,不是 `src` 的依赖**——`./typert` 与 `./remote` 解析到未打包的 `lib` 文件,其中带有裸的 `import { z } from 'zod'`,所以本包声明它,`knip.json` 也在这个 workspace 里忽略它;`src` 里没有任何代码 import zod。
+- **`zod` 是生成的 Typert 契约面的运行时依赖,不是 `src` 的依赖**——`./typert` 与 `./remote` 解析到未打包的 `lib` 文件,其中带有裸的 `import { z } from 'zod'`,所以即使 `src` 里没有任何代码 import zod,本包也要声明它。
### 开发备注
diff --git a/packages/extensions/cordis-host-runner/package.json b/packages/extensions/cordis-host-runner/package.json
index 6fce78d714..4f49b95c8d 100644
--- a/packages/extensions/cordis-host-runner/package.json
+++ b/packages/extensions/cordis-host-runner/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-cordis-host-runner",
"description": "Dynamic package definition registry, host-half sandbox lifecycle, and invoke handler table for model-mounted dual-half packages",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/extensions/tool-cordis/package.json b/packages/extensions/tool-cordis/package.json
index 6e8428322e..0c1a2c956a 100644
--- a/packages/extensions/tool-cordis/package.json
+++ b/packages/extensions/tool-cordis/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-cordis",
"description": "Self-referential cordis toolset: inspect the live runtime, mount and dispose model-written plugins",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index 4012db1069..28e8d3dc06 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/packages/extensions/tool-cordis/src/api-catalog.ts
@@ -1230,9 +1230,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
description: 'Owns the deployment\'s permission presets and their write path. Requires a confining `ctx.shell` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error.',
methods: [
{
- signature: 'current(events: readonly SessionEvent[]): string',
+ signature: 'current(session: Session): string',
description: 'Resolve the preset matching the effective knob values. A still-matching last selection wins shared-bundle ties; otherwise the first table match wins, or CUSTOM_PRESET when no entry matches.',
- parameters: [{ name: 'events', description: 'the session\'s events in log order.' }],
+ parameters: [{ name: 'session', description: 'the session whose knob state is read.' }],
returns: 'the effective preset name, or `custom` when nothing matches.',
},
{
@@ -1265,7 +1265,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
{
key: 'planMode',
summary: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool.',
- description: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool. UIs observe committed flips through `session/event`; there is no live mirror.',
+ description: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool. Client carriers expose the projection\'s cropped `{ active, pending }` view.',
methods: [
{
signature: 'get(agent: Agent): { active: boolean; pending?: boolean }',
@@ -1568,7 +1568,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
{
key: 'sessionProjections',
summary: '`ctx.sessionProjections`: the projection unit table and its drive.',
- description: '`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 in a client-visible unit 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.',
+ description: '`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 in a client-visible unit 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. A host reader either declares `sessionProjections` in its plugin `inject` or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through `ctx.inject([\'sessionProjections\'], ...)`. 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.',
methods: [
{
signature: 'register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, \'wire\'> & { wire: NonNullable[\'wire\']> }, ): () => void',
@@ -2501,7 +2501,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
methods: [
{
signature: 'presentAs(mode: ToolPresentationMode): () => void',
- description: 'Present the calling scope\'s tools in `mode` instead of the deployment default. Nearest scope on the chain wins, so a preset\'s standing declaration covers every agent joined under it.\n\nScoped only, and one declaration per scope: this is how an agent preset composes Code Mode agents beside native ones in the same process, and a process-global override would be the `mode` config field instead.',
+ description: 'Present the calling scope\'s tools in `mode` instead of the deployment default. Nearest scope on the chain wins, so a preset\'s standing declaration covers every agent joined under it.\n\nScoped only, and one declaration per scope: this is how an agent preset composes PTC mode agents beside native ones in the same process, and a process-global override would be the `mode` config field instead.',
parameters: [{ name: 'mode', description: 'the presentation the covered agents\' models see.' }],
returns: 'the exact disposer that restores the deployment default.',
},
@@ -3274,14 +3274,6 @@ export const EVENT_API: readonly EventApiEntry[] = [
description: '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.',
parameters: [],
},
- {
- name: 'tools/code-dispatch-log',
- mode: 'waterfall',
- signature: '\'tools/code-dispatch-log\'(this: Scoped, dispatch: CodeDispatchLog, next: () => Promise): Promise',
- summary: 'Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event.',
- description: 'Allow a listener to replace content in 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 original settled content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent\'s dispatches.',
- parameters: [{ name: 'dispatch', description: 'the parent execution, sub-call identity, and the settled content to log.' }],
- },
{
name: 'tools/execute',
mode: 'waterfall',
@@ -3306,6 +3298,14 @@ export const EVENT_API: readonly EventApiEntry[] = [
description: '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.',
parameters: [{ name: 'exec', description: 'the pending call (name, parsed arguments, caller agent).' }],
},
+ {
+ name: 'tools/ptc-dispatch-log',
+ mode: 'waterfall',
+ signature: '\'tools/ptc-dispatch-log\'(this: Scoped, dispatch: PtcDispatchLog, next: () => Promise): Promise',
+ summary: 'Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event.',
+ description: 'Allow a listener to replace content in 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 original settled content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent\'s dispatches.',
+ parameters: [{ name: 'dispatch', description: 'the parent execution, sub-call identity, and the settled content to log.' }],
+ },
{
name: 'tools/result',
mode: 'emit',
@@ -3610,10 +3610,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'CodeBindingNamespace',
declaration: 'export interface CodeBindingNamespace {\n global: string;\n functions: Record;\n errorClass?: CodeBindingErrorClass;\n}',
},
- {
- name: 'CodeDispatchLog',
- declaration: 'export interface CodeDispatchLog {\n readonly exec: ToolExecution;\n readonly agent?: Agent;\n readonly subCallId: ToolCallId;\n readonly name: string;\n readonly isError: boolean;\n readonly content: ContentBlock[];\n}',
- },
{
name: 'CodeJsonValue',
declaration: 'export type CodeJsonValue = null | boolean | number | string | CodeJsonValue[] | {\n [key: string]: CodeJsonValue;\n};',
@@ -4582,6 +4578,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'PruneResult',
declaration: 'export interface PruneResult {\n readonly pruned: readonly PrunedEntry[];\n readonly charsRemoved: number;\n}',
},
+ {
+ name: 'PtcDispatchLog',
+ declaration: 'export interface PtcDispatchLog {\n readonly exec: ToolExecution;\n readonly agent?: Agent;\n readonly subCallId: ToolCallId;\n readonly name: string;\n readonly isError: boolean;\n readonly content: ContentBlock[];\n}',
+ },
{
name: 'ReadFileLine',
declaration: 'export interface ReadFileLine {\n number: number;\n text: string;\n}',
@@ -5130,10 +5130,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'SessionTitleProvider',
declaration: 'export interface SessionTitleProvider {\n readonly id: SessionTitleProviderId;\n readonly automatic: SessionTitleAutomaticMode;\n generate(request: SessionTitleProviderRequest): Promise;\n}',
},
- {
- name: 'SessionTitleProviderId',
- declaration: 'export type SessionTitleProviderId = Branded<\'SessionTitleProviderId\'>;',
- },
{
name: 'SessionTitleProviderRequest',
declaration: 'export interface SessionTitleProviderRequest {\n readonly session: Session;\n readonly messages: readonly SessionTitleUserMessage[];\n readonly route?: SessionTitleModelProvenance;\n readonly signal: AbortSignal;\n}',
@@ -5728,7 +5724,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'ToolPresentationMode',
- declaration: 'export type ToolPresentationMode = \'native\' | \'code\' | \'both\';',
+ declaration: 'export type ToolPresentationMode = \'native\' | \'ptc\' | \'both\';',
},
{
name: 'ToolProviderResult',
diff --git a/packages/extensions/ui-cordis/package.json b/packages/extensions/ui-cordis/package.json
index c71fd086c3..f37b0a8dc5 100644
--- a/packages/extensions/ui-cordis/package.json
+++ b/packages/extensions/ui-cordis/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-cordis",
"description": "Cordis dynamic-plugin definition card: the keyed cordis_define tool row with its run/stop switch",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/feedback/command-feedback/package.json b/packages/feedback/command-feedback/package.json
index ecaf45a615..dac634d5a6 100644
--- a/packages/feedback/command-feedback/package.json
+++ b/packages/feedback/command-feedback/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-command-feedback",
"description": "Log-only session feedback producer and human-facing slash command",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/feedback/message-feedback/package.json b/packages/feedback/message-feedback/package.json
index ecad54a19b..75359357f9 100644
--- a/packages/feedback/message-feedback/package.json
+++ b/packages/feedback/message-feedback/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-message-feedback",
"description": "Lifecycle-bound per-message rating and note sidecar for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json
index 58d86392e5..b267d2da82 100644
--- a/packages/fs/fs-local/package.json
+++ b/packages/fs/fs-local/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-fs-local",
"description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/fs/fs-observation-policy/package.json b/packages/fs/fs-observation-policy/package.json
index 35af55cc86..98d7d9d907 100644
--- a/packages/fs/fs-observation-policy/package.json
+++ b/packages/fs/fs-observation-policy/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-fs-observation-policy",
"description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service API)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/fs/fs-sandbox/package.json b/packages/fs/fs-sandbox/package.json
index c41b249a59..07751b0837 100644
--- a/packages/fs/fs-sandbox/package.json
+++ b/packages/fs/fs-sandbox/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-fs-sandbox",
"description": "Sandbox-enforcing implementation of the DeepSeek Harness filesystem seam: fences write/edit by the per-call sandbox mode (read-only denies mutation, workspace-write contains it to the workspace + temp roots) while reads pass through",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -45,6 +45,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts b/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts
index f6aaa0dd81..435086a301 100644
--- a/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts
+++ b/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts
@@ -17,6 +17,7 @@ import { Context } from '@deepseek-ai/cordis'
import { FsError, FsTargetKey } from '@deepseek-ai/dsh-fs'
import type { FsTarget } from '@deepseek-ai/dsh-fs'
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
import { SandboxedFileSystem } from '@deepseek-ai/dsh-fs-sandbox'
@@ -29,6 +30,7 @@ let fiber: Awaited>
async function boot(mode: SandboxMode): Promise {
ctx = new Context()
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace })
fiber = await ctx.plugin(SandboxedFileSystem, { cwd: workspace })
fs = ctx.fs as SandboxedFileSystem
@@ -171,6 +173,7 @@ describe('workspace-write with the filesystem root as the workspace (a root endi
// A degenerate but valid config: the filesystem root containing the target.
// It exercises the separator-suffixed-root branch on POSIX and Windows.
const rootCtx = new Context()
+ await rootCtx.plugin(SessionProjectionRegistry)
await rootCtx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: parse(base).root })
const rootFiber = await rootCtx.plugin(SandboxedFileSystem, { cwd: workspace })
const rootFs = rootCtx.fs as SandboxedFileSystem
diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json
index 1d4f51dfd9..c86055e99b 100644
--- a/packages/fs/fs/package.json
+++ b/packages/fs/fs/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-fs",
"description": "Abstract filesystem capability seam (ctx.fs) for the DeepSeek Harness — vocabulary types, the FileSystem service (text IO + optional version-guarded atomic mutations), and the fs/* policy event vocabulary",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/fs/tool-fs-search/package.json b/packages/fs/tool-fs-search/package.json
index 8b265eb6bc..fb86a437de 100644
--- a/packages/fs/tool-fs-search/package.json
+++ b/packages/fs/tool-fs-search/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-fs-search",
"description": "Model-facing filesystem discovery tools (glob, grep) backed by the packaged ripgrep binary (@vscode/ripgrep)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json
index 7214e5fa46..64d2c88d36 100644
--- a/packages/fs/tool-fs/package.json
+++ b/packages/fs/tool-fs/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-fs",
"description": "Model-facing filesystem tools (read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -65,6 +65,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/fs/tool-fs/tests/harness.ts b/packages/fs/tool-fs/tests/harness.ts
index 2e6dc22cf8..f226e47b71 100644
--- a/packages/fs/tool-fs/tests/harness.ts
+++ b/packages/fs/tool-fs/tests/harness.ts
@@ -1,6 +1,7 @@
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
@@ -14,6 +15,7 @@ import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
*/
export async function fsHarness(fsCwd: string, persona = ''): Promise {
const ctx = new Context()
+ await ctx.plugin(SessionProjectionRegistry)
await mountAgentLoopTestDependencies(ctx, { systemPrompt: { persona } })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LlmDeepSeek)
diff --git a/packages/fs/tool-fs/tests/read-image.spec.ts b/packages/fs/tool-fs/tests/read-image.spec.ts
index 583685a699..7244444245 100644
--- a/packages/fs/tool-fs/tests/read-image.spec.ts
+++ b/packages/fs/tool-fs/tests/read-image.spec.ts
@@ -65,7 +65,7 @@ class CatalogAdapter extends LlmAdapter {
}
}
-/** In-process Code Mode seam fake that invokes the real registry bindings. */
+/** In-process PTC mode seam fake that invokes the real registry bindings. */
class FakeRuntime extends CodeRuntime {
readonly language = 'typescript'
readonly isolation = 'fake'
@@ -101,7 +101,7 @@ async function setup(options: SetupOptions = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime, { mode: options.toolMode ?? 'native' })
- if (options.toolMode === 'code' || options.toolMode === 'both') {
+ if (options.toolMode === 'ptc' || options.toolMode === 'both') {
await ctx.plugin(FakeRuntime)
}
await ctx.plugin(LocalFileSystem, { cwd: dir })
@@ -226,9 +226,9 @@ describe('read_image happy path', () => {
expect(result.isError).toBe(false)
})
- it('forwards a nested Code Mode image through the outer run_code context', async () => {
+ it('forwards a nested PTC mode image through the outer run_code context', async () => {
await writeFile(join(dir, 'red.png'), PNG_1X1)
- const ctx = await setup({ toolMode: 'code' })
+ const ctx = await setup({ toolMode: 'ptc' })
const runtime = ctx.codeRuntime as FakeRuntime
runtime.behavior = async (request) => {
const value = await request.bindings[0]!.functions.read_image!({ file_path: 'red.png' })
@@ -237,7 +237,7 @@ describe('read_image happy path', () => {
const result = await call(ctx, RUN_CODE_NAME, {
code: 'return await tools.read_image({ file_path: "red.png" })',
- description: 'Read the image through Code Mode',
+ description: 'Read the image through PTC mode',
}, agentOn('vision-model'))
expect(result.isError).toBe(false)
diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts
index 6a1fc07bba..5e50e7fc4e 100644
--- a/packages/fs/tool-fs/tests/tools.spec.ts
+++ b/packages/fs/tool-fs/tests/tools.spec.ts
@@ -8,6 +8,7 @@ import { Context } from '@deepseek-ai/cordis'
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve, sep } from 'node:path'
+import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { type ToolResult } from '@deepseek-ai/dsh-tools'
@@ -31,6 +32,7 @@ import { sessionCwd } from '../src/session-cwd.ts'
import ApprovalService from '@deepseek-ai/dsh-user-approval'
import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
const testToolSignal = new AbortController().signal
@@ -792,6 +794,8 @@ describe('sandbox escalation API (write/edit)', () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
+ await ctx.plugin(SessionProjectionRegistry)
+ ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write' })
await ctx.plugin(SandboxingFakeFs)
await ctx.plugin(FsPolicy)
@@ -806,7 +810,7 @@ describe('sandbox escalation API (write/edit)', () => {
id: 'agent-fs-esc',
session: {
header: { version: 0, id: 'sess-fs-esc', createdAt: 0, cwd: '/session-project' },
- events: [{ type: 'turn/start' }, ...events],
+ events: [{ type: 'turn/start', data: { turn: 1 } }, ...events],
append: (type: string, data: Record) => { events.push({ type, data }) },
},
}
diff --git a/packages/fs/tool-str-replace-editor/package.json b/packages/fs/tool-str-replace-editor/package.json
index a055f01752..c97dd886af 100644
--- a/packages/fs/tool-str-replace-editor/package.json
+++ b/packages/fs/tool-str-replace-editor/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-str-replace-editor",
"description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -54,6 +54,7 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts
index 1523de3d12..5d1faafea3 100644
--- a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts
+++ b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts
@@ -12,6 +12,7 @@ import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox'
import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import * as ToolStrReplaceEditor from '@deepseek-ai/dsh-tool-str-replace-editor'
@@ -76,6 +77,9 @@ async function setup(
if (options.sandboxMode === undefined) {
await ctx.plugin(LocalFileSystem, { cwd: root })
} else {
+ // SandboxPolicy declares the registry as a required injection; mount it
+ // before the policy activates.
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SandboxPolicy, { mode: options.sandboxMode, workspaceRoot: root })
await ctx.plugin(SandboxedFileSystem, { cwd: root })
}
diff --git a/packages/goal/command-goal/package.json b/packages/goal/command-goal/package.json
index fe0edc727d..7f0acdca81 100644
--- a/packages/goal/command-goal/package.json
+++ b/packages/goal/command-goal/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-command-goal",
"description": "Human-facing slash command for persisted same-session goals",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -46,6 +46,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/goal/command-goal/tests/command-goal.spec.ts b/packages/goal/command-goal/tests/command-goal.spec.ts
index 2127844646..ed126cc02e 100644
--- a/packages/goal/command-goal/tests/command-goal.spec.ts
+++ b/packages/goal/command-goal/tests/command-goal.spec.ts
@@ -7,6 +7,7 @@ import CommandRuntime from '@deepseek-ai/dsh-commands'
import GoalService from '@deepseek-ai/dsh-goal'
import type { GoalRef } from '@deepseek-ai/dsh-goal'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as commandGoal from '@deepseek-ai/dsh-command-goal'
interface Harness {
@@ -46,6 +47,7 @@ async function harness(): Promise {
await ctx.plugin(SessionStore)
await ctx.plugin(CommandRuntime)
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(GoalService)
const plugin = await ctx.plugin(commandGoal)
const { agent, session } = stubAgent(ctx, `command-goal-${Math.random()}`)
diff --git a/packages/goal/goal-round-driver/package.json b/packages/goal/goal-round-driver/package.json
index eb16656526..c897f906f3 100644
--- a/packages/goal/goal-round-driver/package.json
+++ b/packages/goal/goal-round-driver/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-goal-round-driver",
"description": "Race-fenced same-session goal-round driver",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -49,6 +49,7 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/goal/goal-round-driver/tests/goal-round-driver.spec.ts b/packages/goal/goal-round-driver/tests/goal-round-driver.spec.ts
index 2a4059c2ea..f92db57dbf 100644
--- a/packages/goal/goal-round-driver/tests/goal-round-driver.spec.ts
+++ b/packages/goal/goal-round-driver/tests/goal-round-driver.spec.ts
@@ -10,6 +10,7 @@ import { createUserMessage, LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { UserMessage } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as goalSession from '../src/index.ts'
type ScriptEntry = StreamChunk[] | Error | 'hang' | ((options: GenerateOptions) => StreamChunk[])
@@ -89,6 +90,7 @@ async function harness(script: ScriptEntry[]): Promise {
const ctx = new Context()
contexts.push(ctx)
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(GoalService)
const driver = await ctx.plugin(goalSession)
await ctx.plugin(AgentLoop, { agents: [] })
@@ -215,6 +217,7 @@ describe('same-session goal driving', () => {
const ctx = new Context()
contexts.push(ctx)
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(GoalService)
await ctx.plugin(AgentLoop, { agents: [] })
const adapter = new ScriptedAdapter([textResponse('after resume')])
diff --git a/packages/goal/goal/README.i18n.yaml b/packages/goal/goal/README.i18n.yaml
index 3fc4234199..f2182bb164 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: e6abcb0313da091237a64fb7fa92ec80ff40144d
-README.zh.md: eb64366c64571aace7fa7a1db5ac9fe713cae65f
+README.md: 1d1fb17db63c15cba2a0e4244352ad566314d355
+README.zh.md: a96a574894b4e080605418d3055802fd76080bf1
diff --git a/packages/goal/goal/README.md b/packages/goal/goal/README.md
index e6abcb0313..1d1fb17db6 100644
--- a/packages/goal/goal/README.md
+++ b/packages/goal/goal/README.md
@@ -94,7 +94,7 @@ This section explains how the service realizes the behavior above; the observabl
- **Compare-and-set mutations.** `ctx.goals` accepts only the exact live `Agent` registered under its id. `get()` returns a detached `GoalView`; mutations take a `GoalRef { id, revision }` and reject stale refs. Creation resolves the deployment default internally before committing.
- **Activation is process-local.** `armed` and `disarmed` live in a per-session cache and are never persisted. A fresh cache and every `agent/session-start` edge disarm continuation even when replay finds an active durable phase; `disarm()` removes authority without writing a revision or emitting a mutation.
- **Strict replay.** The fold derives lifecycle mutations only from `goal/change` and rejects malformed shapes, discontinuous revisions, illegal phase transitions, non-monotonic per-goal timestamps, and non-sequential admitted rounds. Positive rounds advance only on admitted goal-sourced `user/message` events, and mutation timestamps clamp against the preceding update when wall time moves backward.
-- **Projection unit.** The package registers a last-wins `goal` projection (the whole current goal, or `null`) that activates only when a projection registry is composed.
+- **Projection unit.** The package registers a last-wins `goal` projection (the whole current goal, or `null`) that activates only when a projection registry is composed. `GoalService` reads this unit and fails on the first dependent access if the registry or key is absent.
### Source map
diff --git a/packages/goal/goal/README.zh.md b/packages/goal/goal/README.zh.md
index eb64366c64..a96a574894 100644
--- a/packages/goal/goal/README.zh.md
+++ b/packages/goal/goal/README.zh.md
@@ -94,7 +94,7 @@ view.activation // 'armed' | 'disarmed' — not persisted
- **比较并设置的变更。** `ctx.goals` 只接受以对应 id 注册的完全相同的活跃 `Agent` 实例。`get()` 返回脱离状态的 `GoalView`;变更携带 `GoalRef { id, revision }` 并拒绝陈旧引用。创建在提交前于内部解析部署默认值。
- **续行启用状态是进程本地的。** `armed` 与 `disarmed` 保存在每会话缓存中,绝不持久化。新缓存与每次 `agent/session-start` 边界都会停用续行,即使回放发现持久 phase 为 active;`disarm()` 移除续行权限,不写入 revision 也不发出变更事件。
- **严格回放。** 折叠只从 `goal/change` 派生生命周期变更,并拒绝形状错误、不连续 revision、非法 phase 转换、每目标时间戳非单调,以及不连续的已准入 Round。只有已准入的来源为 goal 的 `user/message` 事件会推进正数 Round;挂钟时间倒退时,变更时间戳会限制在不早于上一次更新的值。
-- **投影单元。** 本包注册一个 last-wins 的 `goal` 投影(完整当前 goal,或 `null`),仅当组合了投影注册表时才激活。
+- **投影单元。** 本包注册一个 last-wins 的 `goal` 投影(完整当前 goal,或 `null`),仅当组合了投影注册表时才激活。`GoalService` 读取该单元;注册表或 key 缺失时,第一次依赖它们的访问会失败。
### 源码地图
diff --git a/packages/goal/goal/package.json b/packages/goal/goal/package.json
index 5082782244..fa54234e83 100644
--- a/packages/goal/goal/package.json
+++ b/packages/goal/goal/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-goal",
"description": "Event-sourced same-session goal state and lifecycle service for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/goal/goal/src/index.ts b/packages/goal/goal/src/index.ts
index baf8c921a9..7d16a7dfe6 100644
--- a/packages/goal/goal/src/index.ts
+++ b/packages/goal/goal/src/index.ts
@@ -13,12 +13,10 @@ import { agentEvents } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol'
-// Type-only: resolves ctx.sessionProjections for the optional unit child.
import type {} from '@deepseek-ai/dsh-session-projection'
+import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import {
applyGoalEvent,
- decodeGoalChange,
- emptyGoalFoldState,
goalChangeRef,
} from './fold.ts'
import type { GoalFoldState } from './fold.ts'
@@ -35,6 +33,7 @@ import type {
GoalBlockReason,
GoalPhase,
GoalProjection,
+ GoalProjectionState,
GoalRef,
GoalSnapshot,
GoalView,
@@ -62,7 +61,7 @@ declare module '@deepseek-ai/cordis' {
}
}
-/** Wire payload schema of the `goal` projection (whole current goal or pre-create/cleared null). */
+/** Wire payload schema of the `goal` projection (current goal or pre-create/cleared null). */
const goalProjectionSchema: ZodType = zod.union([
zod.object({
goal: zod.object({
@@ -80,38 +79,94 @@ const goalProjectionSchema: ZodType = zod.union([
zod.null(),
]) as ZodType
+const goalProjectionStateSchema: ZodType = zod.object({
+ current: goalProjectionSchema,
+ seenGoalIds: zod.array(zod.string().min(1)).refine(
+ ids => new Set(ids).size === ids.length,
+ { message: 'seen goal ids must be unique' },
+ ),
+ failure: zod.string().min(1).nullable(),
+}).strict().superRefine((state, context) => {
+ if (state.current === null) return
+ if (!state.seenGoalIds.includes(state.current.goal.id)) {
+ context.addIssue({ code: 'custom', message: 'current goal id must be retained among seen goal ids' })
+ }
+ if (state.current.updatedAt < state.current.createdAt) {
+ context.addIssue({ code: 'custom', message: 'current goal update cannot precede its creation' })
+ }
+ if (state.current.roundsStarted > state.current.goal.maxGoalRounds) {
+ context.addIssue({ code: 'custom', message: 'current goal rounds cannot exceed its configured limit' })
+ }
+}) as unknown as ZodType
+
+/** Build strict fold state from one checkpoint-safe projection state. */
+function goalFoldState(state: GoalProjectionState): GoalFoldState {
+ return {
+ goal: state.current?.goal,
+ roundsStarted: state.current?.roundsStarted ?? 0,
+ createdAt: state.current?.createdAt,
+ updatedAt: state.current?.updatedAt,
+ lastRef: undefined,
+ seenGoalIds: new Set(state.seenGoalIds),
+ }
+}
+
+/** Convert strict fold state into checkpoint-safe projection state. */
+function goalProjectionState(state: GoalFoldState): GoalProjectionState {
+ let current: GoalProjection | null = null
+ if (state.goal !== undefined) {
+ const { createdAt, updatedAt } = state
+ if (createdAt === undefined || updatedAt === undefined) {
+ throw new Error('current goal fold lacks timestamps')
+ }
+ current = {
+ goal: state.goal,
+ roundsStarted: state.roundsStarted,
+ createdAt,
+ updatedAt,
+ }
+ }
+ return {
+ current,
+ seenGoalIds: [...state.seenGoalIds],
+ failure: null,
+ }
+}
+
/**
- * Light last-wins fold of the `goal` projection unit. Unlike the strict
- * replay fold (fold.ts: transition validation, fail-loud on malformed
- * changes, Set-typed state), this transition is projection-grade: the state
- * is plain JSON (persisted-cache precondition), any non-goal or malformed
- * event returns the same reference (the registry's Object.is gate — the
- * title/todos posture), and correctness of the written change is the write
- * side's job (GoalService validated it before appending; the package
- * invariant rejects a violating stream fail-loud where it is installed).
+ * Fold durable goal events through the strict replay rules without throwing
+ * from the projection registry's event drive. The first invalid owned event
+ * is retained in `failure`; host goal access rejects that state while the
+ * client view remains at the last valid goal.
* @param state - the projection covering all prior events.
* @param event - the next committed session event.
- * @returns the next projection (same reference when the event is not a goal change).
+ * @returns the next projection (same reference when the event is unrelated).
*/
-export function applyGoalProjection(state: GoalProjection | null, event: SessionEvent): GoalProjection | null {
- if (event.type !== 'goal/change') return state
- let change: GoalChangeMeta | undefined
+export function applyGoalProjection(state: GoalProjectionState, event: SessionEvent): GoalProjectionState {
+ if (state.failure !== null) return state
+ if (event.type !== 'goal/change'
+ && (event.type !== 'user/message' || event.data.source.kind !== 'goal')) return state
+ const folded = goalFoldState(state)
try {
- change = decodeGoalChange(event.data)
- } catch (_invalidPersistedGoalChange) {
- return state
+ applyGoalEvent(folded, event)
+ return goalProjectionState(folded)
+ } catch (error: unknown) {
+ /* v8 ignore next -- the strict goal fold throws Error instances. */
+ const message = error instanceof Error ? error.message : String(error)
+ return { ...state, failure: `goal replay failed at session event ${event.seq}: ${message}` }
}
- if (change === undefined) return state
- return change.operation === 'clear'
- ? null
- : {
- goal: change.goal,
- roundsStarted: change.roundsStarted,
- createdAt: change.createdAt,
- updatedAt: change.updatedAt,
- }
}
+/** Strict host goal state with the existing cropped client value. */
+export const goalProjectionDefinition = {
+ key: 'goal',
+ stateSchema: goalProjectionStateSchema,
+ init: (): GoalProjectionState => ({ current: null, seenGoalIds: [], failure: null }),
+ apply: applyGoalProjection,
+ wire: { viewSchema: goalProjectionSchema, view: state => state.current },
+ stateVersion: 6,
+} satisfies ProjectionDefinition<'goal', GoalProjectionState>
+
/** Deployment defaults for goal creation. */
export interface Config {
/** Total rounds used when a create request omits its own cap. */
@@ -124,11 +179,9 @@ export interface ResolvedConfig {
defaultMaxGoalRounds: number
}
-/** Process-local cache plus activation intent crossing the synchronous append boundary. */
-interface GoalCache {
- readonly state: GoalFoldState
+/** Process-local activation state crossing the synchronous append boundary. */
+interface GoalRuntimeState {
activation: GoalActivation
- observedSeq: number
pendingActivation: { readonly seq: number; readonly activation: GoalActivation } | undefined
}
@@ -188,7 +241,7 @@ export class GoalService extends TypertRemoteService {
})
private readonly resolved: ResolvedConfig
- private readonly caches = new WeakMap()
+ private readonly runtimeStates = new WeakMap()
constructor(ctx: Context, config: Config = {}) {
super(ctx, 'goals')
@@ -196,20 +249,17 @@ export class GoalService extends TypertRemoteService {
defaultMaxGoalRounds: resolveMaxGoalRounds(config.defaultMaxGoalRounds ?? 256),
}
ctx.on('agent/session-start', ({ agent }) => {
- this.cache(agent.session).activation = 'disarmed'
+ this.runtimeState(agent.session).activation = 'disarmed'
})
- // The `goal` projection unit: last-wins fold of goal/change whole values
- // (see applyGoalProjection). The unit child activates only when a
- // projection registry is composed (headless assemblies stay unaffected).
ctx.inject(['sessionProjections'], (projectionCtx) => {
- projectionCtx.sessionProjections.register<'goal', GoalProjection | null>({
- key: 'goal',
- stateSchema: goalProjectionSchema,
- init: () => null,
- apply: applyGoalProjection,
- wire: { viewSchema: goalProjectionSchema, view: state => state },
- stateVersion: 4,
- })
+ projectionCtx.sessionProjections.register(goalProjectionDefinition)
+ })
+ ctx.on('session/event', (session, event) => {
+ if (event.type !== 'goal/change') return
+ const runtime = this.runtimeState(session)
+ runtime.activation = runtime.pendingActivation?.seq === event.seq
+ ? runtime.pendingActivation.activation
+ : 'disarmed'
})
}
@@ -221,9 +271,7 @@ export class GoalService extends TypertRemoteService {
*/
get(agent: Agent): GoalView | undefined {
this.assertLive(agent)
- const cache = this.cache(agent.session)
- this.sync(agent.session, cache)
- return this.view(cache)
+ return this.view(this.state(agent.session), this.runtimeState(agent.session))
}
/**
@@ -235,10 +283,9 @@ export class GoalService extends TypertRemoteService {
*/
disarm(agent: Agent): GoalView | undefined {
this.assertLive(agent)
- const cache = this.cache(agent.session)
- this.sync(agent.session, cache)
- cache.activation = 'disarmed'
- return this.view(cache)
+ const runtime = this.runtimeState(agent.session)
+ runtime.activation = 'disarmed'
+ return this.view(this.state(agent.session), runtime)
}
/**
@@ -250,8 +297,8 @@ export class GoalService extends TypertRemoteService {
*/
create(agent: Agent, request: CreateGoalRequest): GoalView {
const spec = resolveCreateGoal(request, this.resolved.defaultMaxGoalRounds)
- const cache = this.prepareMutation(agent)
- const current = cache.state.goal
+ const [state, runtime] = this.prepareMutation(agent)
+ const current = state?.goal
if (current !== undefined && current.phase !== 'complete') {
throw new GoalError(`goal "${current.id}" already exists with phase "${current.phase}"`, 'GOAL_ALREADY_EXISTS')
}
@@ -263,7 +310,7 @@ export class GoalService extends TypertRemoteService {
phase: 'active',
maxGoalRounds: spec.maxGoalRounds,
}
- return this.commitSnapshot(agent, cache, 'create', goal, 0, now, now, 'armed')
+ return this.commitSnapshot(agent, runtime, 'create', goal, 0, now, now, 'armed')
}
/**
@@ -275,8 +322,9 @@ export class GoalService extends TypertRemoteService {
*/
@Remote('edit')
edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView {
- const cache = this.prepareMutation(agent)
- const current = this.expectCurrent(cache, ref)
+ const [state, runtime] = this.prepareMutation(agent)
+ const currentState = this.expectCurrent(state, ref)
+ const current = currentState.goal
if (request.objective === undefined && request.maxGoalRounds === undefined) {
throw new GoalError('goal edit requires objective and/or maxGoalRounds', 'GOAL_INVALID_EDIT')
}
@@ -286,7 +334,7 @@ export class GoalService extends TypertRemoteService {
...request.objective === undefined ? {} : { objective: resolveObjective(request.objective) },
...request.maxGoalRounds === undefined ? {} : { maxGoalRounds: resolveMaxGoalRounds(request.maxGoalRounds) },
}
- return this.commitCurrent(agent, cache, 'edit', goal, cache.activation)
+ return this.commitCurrent(agent, currentState, runtime, 'edit', goal, runtime.activation)
}
/**
@@ -309,22 +357,23 @@ export class GoalService extends TypertRemoteService {
*/
@Remote('resume')
resume(agent: Agent, ref: GoalRef): GoalView {
- const cache = this.prepareMutation(agent)
- const current = this.expectCurrent(cache, ref)
+ const [state, runtime] = this.prepareMutation(agent)
+ const currentState = this.expectCurrent(state, ref)
+ const current = currentState.goal
const resumable: readonly GoalPhase[] = ['active', 'paused', 'blocked']
if (!resumable.includes(current.phase)) {
throw this.transitionError(current, 'resume', resumable)
}
- if (current.phase === 'active' && cache.activation === 'armed') {
+ if (current.phase === 'active' && runtime.activation === 'armed') {
throw new GoalError(`goal "${current.id}" is already active and armed`, 'GOAL_INVALID_TRANSITION')
}
- if (cache.state.roundsStarted >= current.maxGoalRounds) {
+ if (currentState.roundsStarted >= current.maxGoalRounds) {
throw new GoalError(
`goal "${current.id}" exhausted ${current.maxGoalRounds} goal rounds; increase maxGoalRounds before resuming`,
'GOAL_INVALID_TRANSITION',
)
}
- return this.commitCurrent(agent, cache, 'resume', this.withPhase(current, 'active'), 'armed')
+ return this.commitCurrent(agent, currentState, runtime, 'resume', this.withPhase(current, 'active'), 'armed')
}
/**
@@ -353,14 +402,16 @@ export class GoalService extends TypertRemoteService {
* @returns the blocked view with its durable reason.
*/
block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView {
- const cache = this.prepareMutation(agent)
- const current = this.expectCurrent(cache, ref)
+ const [state, runtime] = this.prepareMutation(agent)
+ const currentState = this.expectCurrent(state, ref)
+ const current = currentState.goal
if (current.phase !== 'active') {
throw this.transitionError(current, 'block', ['active'])
}
return this.commitCurrent(
agent,
- cache,
+ currentState,
+ runtime,
'block',
{ ...this.withPhase(current, 'blocked'), blockedReason: resolveBlockReason(reason) },
'disarmed',
@@ -375,39 +426,38 @@ export class GoalService extends TypertRemoteService {
*/
@Remote('clear')
clear(agent: Agent, ref: GoalRef): GoalRef {
- const cache = this.prepareMutation(agent)
- const current = this.expectCurrent(cache, ref)
+ const [state, runtime] = this.prepareMutation(agent)
+ const currentState = this.expectCurrent(state, ref)
+ const current = currentState.goal
const tombstone: GoalRef = { id: current.id, revision: current.revision + 1 }
const change: GoalClearChangeMeta = {
kind: 'goal/change',
version: GOAL_CHANGE_VERSION,
operation: 'clear',
cleared: tombstone,
- clearedAt: this.nextMutationTime(cache),
+ clearedAt: this.nextMutationTime(currentState),
}
- this.commit(agent, cache, change, 'disarmed')
+ this.commit(agent, runtime, change, 'disarmed')
return { ...tombstone }
}
- /** Resolve and validate the cache used by a mutation. */
- private prepareMutation(agent: Agent): GoalCache {
+ /** Resolve the durable and process-local state used by a mutation. */
+ private prepareMutation(agent: Agent): readonly [GoalProjection | null, GoalRuntimeState] {
this.assertLive(agent)
- const cache = this.cache(agent.session)
- this.sync(agent.session, cache)
- return cache
+ return [this.state(agent.session), this.runtimeState(agent.session)]
}
/** Reject stale or missing current-state refs. */
- private expectCurrent(cache: GoalCache, ref: GoalRef): GoalSnapshot {
- const current = cache.state.goal
- if (current === undefined) throw new GoalError('no current goal', 'GOAL_NOT_FOUND')
+ private expectCurrent(state: GoalProjection | null, ref: GoalRef): GoalProjection {
+ if (state === null) throw new GoalError('no current goal', 'GOAL_NOT_FOUND')
+ const current = state.goal
if (ref.id !== current.id || ref.revision !== current.revision) {
throw new GoalError(
`stale goal ref "${ref.id}" revision ${ref.revision}; current is "${current.id}" revision ${current.revision}`,
'GOAL_STALE_REVISION',
)
}
- return current
+ return state
}
/** Enforce exact live-agent identity rather than trusting a matching id. */
@@ -417,33 +467,26 @@ export class GoalService extends TypertRemoteService {
}
}
- /** Return the per-session cache, folding a seed once with activation disarmed. */
- private cache(session: Session): GoalCache {
- let cache = this.caches.get(session)
- if (cache !== undefined) return cache
- const state = emptyGoalFoldState()
- for (const event of session.events) applyGoalEvent(state, event)
- cache = {
- state,
- activation: 'disarmed',
- observedSeq: session.seq,
- pendingActivation: undefined,
- }
- this.caches.set(session, cache)
- return cache
+ /** Read the current durable projection or fail at the first service access. */
+ private state(session: Session): GoalProjection | null {
+ const projections = this.ctx.get('sessionProjections')
+ if (projections === undefined) throw new Error('goal: session projection registry is unavailable')
+ const state = projections.stateOf(session, 'goal')
+ if (state === undefined) throw new Error('goal projection is not registered')
+ if (state.failure !== null) throw new Error(state.failure)
+ return state.current
}
- /** Incrementally observe durable events and reconcile local activation intent. */
- private sync(session: Session, cache: GoalCache): void {
- for (const event of session.events.slice(cache.observedSeq)) {
- applyGoalEvent(cache.state, event)
- if (event.type === 'goal/change') {
- cache.activation = cache.pendingActivation?.seq === event.seq
- ? cache.pendingActivation.activation
- : 'disarmed'
- }
- cache.observedSeq += 1
+ /** Return the process-local activation state, initially disarmed. */
+ private runtimeState(session: Session): GoalRuntimeState {
+ let runtime = this.runtimeStates.get(session)
+ if (runtime !== undefined) return runtime
+ runtime = {
+ activation: 'disarmed',
+ pendingActivation: undefined,
}
+ this.runtimeStates.set(session, runtime)
+ return runtime
}
/** Build a new revision with one replacement phase. */
@@ -466,10 +509,11 @@ export class GoalService extends TypertRemoteService {
phase: GoalPhase,
activation: GoalActivation,
): GoalView {
- const cache = this.prepareMutation(agent)
- const current = this.expectCurrent(cache, ref)
+ const [state, runtime] = this.prepareMutation(agent)
+ const currentState = this.expectCurrent(state, ref)
+ const current = currentState.goal
if (!allowed.includes(current.phase)) throw this.transitionError(current, operation, allowed)
- return this.commitCurrent(agent, cache, operation, this.withPhase(current, phase), activation)
+ return this.commitCurrent(agent, currentState, runtime, operation, this.withPhase(current, phase), activation)
}
/** Render a stable invalid-transition error. */
@@ -483,38 +527,33 @@ export class GoalService extends TypertRemoteService {
/** Commit a mutation that retains the current goal's derived counters/times. */
private commitCurrent(
agent: Agent,
- cache: GoalCache,
+ state: GoalProjection,
+ runtime: GoalRuntimeState,
operation: Exclude,
goal: GoalSnapshot,
activation: GoalActivation,
): GoalView {
- const createdAt = cache.state.createdAt
- /* v8 ignore next -- strict replay and every snapshot commit set createdAt whenever a current goal exists */
- if (createdAt === undefined) throw new Error('current goal cache lacks createdAt')
return this.commitSnapshot(
agent,
- cache,
+ runtime,
operation,
goal,
- cache.state.roundsStarted,
- createdAt,
- this.nextMutationTime(cache),
+ state.roundsStarted,
+ state.createdAt,
+ this.nextMutationTime(state),
activation,
)
}
/** Clamp a current goal's next timestamp across backward wall-clock movement. */
- private nextMutationTime(cache: GoalCache): number {
- const updatedAt = cache.state.updatedAt
- /* v8 ignore next -- strict replay and every snapshot commit set updatedAt whenever a current goal exists */
- if (updatedAt === undefined) throw new Error('current goal cache lacks updatedAt')
- return Math.max(Date.now(), updatedAt)
+ private nextMutationTime(state: GoalProjection): number {
+ return Math.max(Date.now(), state.updatedAt)
}
/** Build and commit one full-snapshot mutation. */
private commitSnapshot(
agent: Agent,
- cache: GoalCache,
+ runtime: GoalRuntimeState,
operation: Exclude,
goal: GoalSnapshot,
roundsStarted: number,
@@ -531,24 +570,28 @@ export class GoalService extends TypertRemoteService {
createdAt,
updatedAt,
}
- this.commit(agent, cache, change, activation)
- const view = this.view(cache)
- /* v8 ignore next -- the durable goal event installs the snapshot before this read */
- if (view === undefined) throw new Error('snapshot commit cleared the goal unexpectedly')
- return view
+ this.commit(agent, runtime, change, activation)
+ return {
+ ...goal,
+ roundsStarted,
+ createdAt,
+ updatedAt,
+ activation: runtime.activation,
+ }
}
- /** Commit one mutation into the goal log, cache, and live event stream. */
- private commit(agent: Agent, cache: GoalCache, change: GoalChangeMeta, activation: GoalActivation): void {
+ /** Commit one mutation into the goal log and live event stream. */
+ private commit(agent: Agent, runtime: GoalRuntimeState, change: GoalChangeMeta, activation: GoalActivation): void {
const ref = goalChangeRef(change)
- cache.pendingActivation = { seq: agent.session.seq, activation }
+ runtime.pendingActivation = { seq: agent.session.seq, activation }
try {
- agent.session.append('goal/change', change)
- this.sync(agent.session, cache)
+ const event = agent.session.append('goal/change', change)
+ /* v8 ignore next -- Session.append returns the event committed at the pre-append seq. */
+ if (runtime.pendingActivation.seq === event.seq) runtime.activation = activation
} finally {
- cache.pendingActivation = undefined
+ runtime.pendingActivation = undefined
}
- const goal = this.view(cache)
+ const goal = this.view(this.state(agent.session), runtime)
const notification: GoalChanged = {
operation: change.operation,
ref: { ...ref },
@@ -558,21 +601,14 @@ export class GoalService extends TypertRemoteService {
}
/** Build a detached current view. */
- private view(cache: GoalCache): GoalView | undefined {
- const goal = cache.state.goal
- const createdAt = cache.state.createdAt
- const updatedAt = cache.state.updatedAt
- if (goal === undefined) return undefined
- /* v8 ignore next 3 -- strict replay and snapshot commits establish both timestamps with every current goal */
- if (createdAt === undefined || updatedAt === undefined) {
- throw new Error(`goal "${goal.id}" cache lacks timestamps`)
- }
+ private view(state: GoalProjection | null, runtime: GoalRuntimeState): GoalView | undefined {
+ if (state === null) return undefined
return {
- ...goal,
- roundsStarted: cache.state.roundsStarted,
- createdAt,
- updatedAt,
- activation: cache.activation,
+ ...state.goal,
+ roundsStarted: state.roundsStarted,
+ createdAt: state.createdAt,
+ updatedAt: state.updatedAt,
+ activation: runtime.activation,
}
}
diff --git a/packages/goal/goal/src/types.ts b/packages/goal/goal/src/types.ts
index 402caade5e..bb7d8b9e1c 100644
--- a/packages/goal/goal/src/types.ts
+++ b/packages/goal/goal/src/types.ts
@@ -84,7 +84,7 @@ export interface GoalView extends GoalSnapshot {
/**
* The `goal` projection value: the current durable goal with its replay
- * counters, exactly as the latest `goal/change` event carried them.
+ * counters, including admitted goal rounds.
* Activation is process-local (never persisted) and deliberately absent —
* the projection reflects durable phase only.
*/
@@ -99,16 +99,26 @@ export interface GoalProjection {
readonly updatedAt: number
}
+/** Strict checkpoint state used to derive the current goal client value. */
+export interface GoalProjectionState {
+ /** Latest valid current goal, or null before creation and after clear. */
+ readonly current: GoalProjection | null
+ /** Goal identities already created in this Session, retained to reject reuse. */
+ readonly seenGoalIds: GoalId[]
+ /** First strict replay failure, or null while the durable stream is valid. */
+ readonly failure: string | null
+}
+
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionStateMap {
- goal: GoalProjection | null
+ goal: GoalProjectionState
}
interface SessionProjectionMap {
/**
- * The session's current goal (the latest `goal/change` whole value), or
+ * The session's current goal and admitted-round count, or
* `null` before the first create and after a clear tombstone.
- * Whole-value rule: every goal change carries the complete post-change
- * state, so the fold is last-wins.
+ * `goal/change` supplies the whole lifecycle value; matching admitted
+ * `user/message` events advance `roundsStarted`.
*/
goal: GoalProjection | null
}
diff --git a/packages/goal/goal/tests/goal.spec.ts b/packages/goal/goal/tests/goal.spec.ts
index e94e0754a0..1c5e90e996 100644
--- a/packages/goal/goal/tests/goal.spec.ts
+++ b/packages/goal/goal/tests/goal.spec.ts
@@ -4,6 +4,7 @@ import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import GoalService, {
GoalError,
GoalId,
@@ -59,9 +60,11 @@ function stubAgent(rawId: string, seed?: readonly import('@deepseek-ai/dsh-sessi
async function harness(config: { defaultMaxGoalRounds?: number } = {}) {
const ctx = new Context()
+ await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(GoalService, config)
- const stub = stubAgent(`goal-test-${Math.random()}`)
+ const stub = stubAgentForSession(ctx.sessions.create(SessionId(`goal-test-${Math.random()}`)))
ctx.agents.register(stub.agent)
return { ctx, ...stub }
}
@@ -78,6 +81,15 @@ function appendRound(session: Session, ref: GoalRef, round: number): void {
}
describe('GoalService creation and replay', () => {
+ it('fails on first state access when the projection registry is absent', async () => {
+ const ctx = new Context()
+ await ctx.plugin(AgentRegistry)
+ await ctx.plugin(GoalService)
+ const stub = stubAgent('goal-missing-projections')
+ ctx.agents.register(stub.agent)
+ expect(() => ctx.goals.get(stub.agent)).toThrow('goal: session projection registry is unavailable')
+ })
+
it('applies the configured default and writes one durable goal change', async () => {
vi.useFakeTimers()
vi.setSystemTime(1_700_000_000_000)
@@ -131,9 +143,12 @@ describe('GoalService creation and replay', () => {
it('also resolves the default when constructed directly without Cordis config normalization', async () => {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
- const goals = new GoalService(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
const stub = stubAgent('goal-direct-construction')
ctx.agents.register(stub.agent)
+ const goals = new GoalService(ctx)
+ expect(() => goals.get(stub.agent)).toThrow('goal projection is not registered')
+ await new Promise(resolve => setImmediate(resolve))
expect(goals.create(stub.agent, { objective: 'direct' })).toMatchObject({
objective: 'direct', maxGoalRounds: 256,
})
@@ -142,6 +157,7 @@ describe('GoalService creation and replay', () => {
it('rejects invalid direct configuration', async () => {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
await expect(ctx.plugin(GoalService, { defaultMaxGoalRounds: -1 })).rejects.toThrow(expect.objectContaining({
code: 'GOAL_INVALID_MAX_ROUNDS',
}))
@@ -155,6 +171,7 @@ describe('GoalService creation and replay', () => {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(GoalService)
const resumed = stubAgent('seeded-goal', first.session.events)
ctx.agents.register(resumed.agent)
@@ -169,6 +186,7 @@ describe('GoalService creation and replay', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(GoalService)
const parent = stubAgentForSession(ctx.sessions.create(SessionId('goal-fork-parent')))
ctx.agents.register(parent.agent)
@@ -212,19 +230,19 @@ describe('GoalService creation and replay', () => {
expect(ctx.goals.resume(agent, goal)).toMatchObject({ revision: 2, activation: 'armed' })
})
- it('removes the service and its session-start listener with the providing fiber', async () => {
+ it('removes the service and projection with the providing fiber', async () => {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
const fiber = await ctx.plugin(GoalService)
const first = ctx.goals
const stub = stubAgent('goal-hmr')
ctx.agents.register(stub.agent)
- const goal = first.create(stub.agent, { objective: 'survive service reload' })
+ const goal = ctx.goals.create(stub.agent, { objective: 'survive service reload' })
await fiber.dispose()
expect(ctx.get('goals')).toBeUndefined()
- agentEvents(ctx, stub.agent).emit('agent/session-start', { source: 'resume' })
- expect(first.get(stub.agent)).toMatchObject({ id: goal.id, activation: 'armed' })
+ expect(ctx.sessionProjections.stateOf(stub.session, 'goal')).toBeUndefined()
await ctx.plugin(GoalService)
expect(ctx.goals).not.toBe(first)
@@ -423,6 +441,7 @@ describe('GoalService mutations', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(GoalService)
const stub = stubAgentForSession(ctx.sessions.create(SessionId('goal-reentrant-observer')))
ctx.agents.register(stub.agent)
@@ -441,6 +460,7 @@ describe('GoalService mutations', () => {
it('does not delegate goal persistence to agent injection', async () => {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(GoalService)
const stub = stubAgent('goal-independent-injection')
stub.agent.inject = () => { throw new Error('injection must not be called') }
@@ -454,23 +474,24 @@ describe('GoalService mutations', () => {
expect(stub.session.events.map(event => event.type)).toEqual(['goal/change'])
})
- it('observes a valid goal snapshot appended after an empty cache was established', async () => {
+ it('observes an external goal change and disarms local activation', async () => {
const { ctx, agent, session } = await harness()
- expect(ctx.goals.get(agent)).toBeUndefined()
+ const created = ctx.goals.create(agent, { objective: 'before external edit', maxGoalRounds: 4 })
+ expect(created.activation).toBe('armed')
const change: GoalSnapshotChangeMeta = {
kind: 'goal/change',
version: 1,
- operation: 'create',
+ operation: 'edit',
goal: {
- id: GoalId('goal-external'),
- revision: 1,
+ id: created.id,
+ revision: created.revision + 1,
objective: 'observe external append',
phase: 'active',
maxGoalRounds: 4,
},
- roundsStarted: 0,
- createdAt: 12,
- updatedAt: 12,
+ roundsStarted: created.roundsStarted,
+ createdAt: created.createdAt,
+ updatedAt: created.updatedAt,
}
session.append('goal/change', change)
@@ -481,30 +502,6 @@ describe('GoalService mutations', () => {
})
})
- it('reports the same corrupt unseen event after committing its valid prefix', async () => {
- const { ctx, agent, session } = await harness()
- expect(ctx.goals.get(agent)).toBeUndefined()
- const change: GoalSnapshotChangeMeta = {
- kind: 'goal/change',
- version: 1,
- operation: 'create',
- goal: {
- id: GoalId('goal-valid-prefix'),
- revision: 1,
- objective: 'valid prefix',
- phase: 'active',
- maxGoalRounds: 4,
- },
- roundsStarted: 0,
- createdAt: 12,
- updatedAt: 12,
- }
- session.append('goal/change', change)
- session.append('goal/change', { ...change, operation: 'edit', extra: true } as never)
-
- expect(() => ctx.goals.get(agent)).toThrow('snapshot change must have exactly')
- expect(() => ctx.goals.get(agent)).toThrow('snapshot change must have exactly')
- })
})
describe('goal replay validation', () => {
diff --git a/packages/goal/goal/tests/projection.spec.ts b/packages/goal/goal/tests/projection.spec.ts
index 6282a6f739..f9ef917584 100644
--- a/packages/goal/goal/tests/projection.spec.ts
+++ b/packages/goal/goal/tests/projection.spec.ts
@@ -3,9 +3,9 @@
* serves the current whole goal on the history tail page with a consistent
* asOfSeq; before the first create the value is null; a clear tombstone
* returns it to null; a composition without the goal service has no `goal`
- * key; unmounting drops it (HMR safety). Malformed goal-shaped events are
- * ignored fail-soft (same-reference return) — strict replay validation
- * belongs to the write side and foldGoal, never the projection drive.
+ * key; unmounting drops it (HMR safety). The host state retains strict replay
+ * failures without throwing from the registry drive, and GoalService rejects
+ * access after such a failure.
*/
import { describe, expect, it, vi } from 'vitest'
@@ -17,8 +17,8 @@ import type { UserMessage } from '@deepseek-ai/dsh-session'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
-import GoalService, { applyGoalProjection, foldGoal } from '@deepseek-ai/dsh-goal'
-import type { GoalRef } from '@deepseek-ai/dsh-goal'
+import GoalService, { GoalId, applyGoalProjection, foldGoal, goalProjectionDefinition } from '@deepseek-ai/dsh-goal'
+import type { GoalProjection, GoalProjectionState, GoalRef } from '@deepseek-ai/dsh-goal'
interface Bench {
ctx: Context
@@ -139,33 +139,59 @@ describe('goal projection unit', () => {
expect(foldGoal(bench.session.events).goal).toBeUndefined()
})
- it('ignores non-goal and malformed goal-shaped events fail-soft (same reference)', () => {
- // The package invariant rejects a violating stream loudly wherever it is
- // installed — the unit itself must never throw on the projection drive
- // (a throwing apply would tear down every registered unit's drive), so
- // its transition is exercised directly as the pure function it is.
+ it('retains strict replay failures without throwing from the projection drive', () => {
const plainUser = createUserMessage({
content: [{ type: 'text', text: 'hi' }],
source: { kind: 'user' },
})
const user = { type: 'user/message', seq: 0, time: 1, data: plainUser } as never
- const state = { goal: { id: 'g1', revision: 1, objective: 'x', phase: 'active', maxGoalRounds: 4 }, roundsStarted: 0, createdAt: 1, updatedAt: 1 } as never
- const empty = null
+ const current: GoalProjection = {
+ goal: { id: GoalId('g1'), revision: 1, objective: 'x', phase: 'active', maxGoalRounds: 4 },
+ roundsStarted: 0,
+ createdAt: 1,
+ updatedAt: 1,
+ }
+ const state: GoalProjectionState = {
+ current,
+ seenGoalIds: [current.goal.id],
+ failure: null,
+ }
+ expect(goalProjectionDefinition.stateSchema.parse(state)).toEqual(state)
+ expect(goalProjectionDefinition.stateSchema.safeParse({ ...state, seenGoalIds: [] }).success).toBe(false)
+ expect(goalProjectionDefinition.stateSchema.safeParse({
+ ...state,
+ current: { ...current, createdAt: 2, updatedAt: 1 },
+ }).success).toBe(false)
+ expect(goalProjectionDefinition.stateSchema.safeParse({
+ ...state,
+ current: { ...current, roundsStarted: current.goal.maxGoalRounds + 1 },
+ }).success).toBe(false)
+ const empty = goalProjectionDefinition.init()
+ expect(goalProjectionDefinition.stateSchema.parse(empty)).toEqual(empty)
expect(applyGoalProjection(empty, user)).toBe(empty)
+ const admittedRound = {
+ type: 'user/message', seq: 1, time: 2,
+ data: createUserMessage({
+ content: [{ type: 'text', text: 'continue' }],
+ source: { kind: 'goal', goalId: 'g1', revision: 1, round: 1 } as never,
+ }),
+ } as never
+ expect(applyGoalProjection(state, admittedRound)).toEqual({
+ ...state,
+ current: { ...current, roundsStarted: 1 },
+ })
const queuedUser = {
type: 'agent/inbox/spliced', seq: 1, time: 2,
data: { target: 'next-step', start: 0, inserted: [plainUser] },
} as never
- const current = state
- expect(applyGoalProjection(current, queuedUser)).toBe(current)
+ expect(applyGoalProjection(state, queuedUser)).toBe(state)
const malformed = {
type: 'goal/change', seq: 1, time: 2,
data: { kind: 'goal/change', version: 1, operation: 'create' },
} as never
- // Same-reference return: the registry's Object.is gate sees no change.
- expect(applyGoalProjection(current, malformed)).toBe(current)
- expect(applyGoalProjection(empty, malformed)).toBe(empty)
+ expect(applyGoalProjection(state, malformed).failure).toMatch(/goal snapshot change must have exactly/)
+ expect(applyGoalProjection(empty, malformed).failure).toMatch(/goal snapshot change must have exactly/)
const queuedRound = {
type: 'agent/inbox/spliced', seq: 3, time: 4,
@@ -174,16 +200,34 @@ describe('goal projection unit', () => {
source: { kind: 'goal', goalId: 'g1', revision: 1, round: 1 } as never,
})] },
} as never
- expect(applyGoalProjection(current, queuedRound)).toBe(current)
+ expect(applyGoalProjection(state, queuedRound)).toBe(state)
// A non-message event (the registry drives EVERY committed event through
// apply): early same-reference return.
const turnStart = { type: 'turn/start', seq: 3, time: 4, data: { turn: 1 } } as never
- expect(applyGoalProjection(current, turnStart)).toBe(current)
+ expect(applyGoalProjection(state, turnStart)).toBe(state)
- // A goal/change event whose payload carries a foreign kind is ignored.
+ // A declared goal/change record with a foreign payload kind is an owned-stream failure.
const foreignKind = { type: 'goal/change', seq: 4, time: 5, data: { kind: 'not-a-goal-change' } } as never
- expect(applyGoalProjection(current, foreignKind)).toBe(current)
+ expect(applyGoalProjection(state, foreignKind).failure).toMatch(/invalid kind/)
+
+ const missingTimestamps = {
+ ...state,
+ current: { ...current, createdAt: undefined, updatedAt: undefined },
+ } as never
+ expect(applyGoalProjection(missingTimestamps, admittedRound).failure)
+ .toMatch(/current goal fold lacks timestamps/)
+ })
+
+ it('fails host goal access when the projection retained a replay failure', async () => {
+ const bench = await harness(true)
+ const failure = 'goal replay failed at session event 0: invalid restored goal stream'
+ const state = bench.ctx.sessionProjections.stateOf(bench.session, 'goal')
+ expect(state).toBeDefined()
+ Object.assign(state!, { failure })
+
+ expect(() => bench.ctx.goals.get(bench.agent)).toThrow(failure)
+ expect(bench.tailValues().goal).toBeNull()
})
it('has no goal key when the goal service is not composed', async () => {
diff --git a/packages/goal/tool-goal/package.json b/packages/goal/tool-goal/package.json
index 576b60b235..cfea9e6154 100644
--- a/packages/goal/tool-goal/package.json
+++ b/packages/goal/tool-goal/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-goal",
"description": "Model-facing same-session goal tools with execution-time authority checks",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -39,7 +39,8 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^"
@@ -47,12 +48,14 @@
"devDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
+ "@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/goal/tool-goal/src/authority.ts b/packages/goal/tool-goal/src/authority.ts
index 1fdfbc740a..bc4ae8faaf 100644
--- a/packages/goal/tool-goal/src/authority.ts
+++ b/packages/goal/tool-goal/src/authority.ts
@@ -6,14 +6,13 @@ import type { GoalView } from '@deepseek-ai/dsh-goal'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
+import type {} from '@deepseek-ai/dsh-session-projection'
-type TurnStartEvent = Extract
-
-/** Current open turn plus the events accepted after its start boundary. */
+/** The calling agent plus the immutable event cut and open-turn start seq used for authority checks. */
export interface GoalToolExecution {
readonly agent: Agent
- readonly start: TurnStartEvent
readonly events: readonly SessionEvent[]
+ readonly openTurnStartSeq: number
}
/** Hard authority granted to one state-changing call. */
@@ -26,26 +25,24 @@ function reject(message: string, code = 'GOAL_TOOL_AUTHORITY_REQUIRED'): never {
throw new HarnessError(message, code)
}
-/** Locate the open turn enclosing a model tool call. */
-function openTurn(agent: Agent): { start: TurnStartEvent; events: readonly SessionEvent[] } {
+/** Resolve the immutable event cut and open-turn boundary without copying the turn suffix. */
+function openTurnEvents(
+ ctx: Context,
+ agent: Agent,
+): Pick {
const events = agent.session.events
- for (let index = events.length - 1; index >= 0; index -= 1) {
- const boundary = events[index]
- if (boundary?.type === 'turn/end') {
- reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED')
- }
- if (boundary?.type === 'turn/start') {
- return { start: boundary, events: events.slice(index + 1) }
- }
+ const boundary = ctx.sessionProjections.stateOf(agent.session, 'turnBoundary')
+ if (boundary === undefined || boundary.openTurnStartSeq === null) {
+ reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED')
}
- return reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED')
+ return { events, openTurnStartSeq: boundary.openTurnStartSeq }
}
/**
* Resolve and authenticate the calling agent and its driver boundary.
* @param ctx - Context carrying the live agent registry.
* @param exec - Tool execution metadata supplied by the registry.
- * @returns The authenticated agent and its current turn window.
+ * @returns The authenticated agent, immutable event cut, and open-turn boundary.
*/
export function goalToolExecution(ctx: Context, exec: ToolRunContext): GoalToolExecution {
const agent = exec.agent
@@ -59,7 +56,19 @@ export function goalToolExecution(ctx: Context, exec: ToolRunContext): GoalToolE
'GOAL_TOOL_DRIVER_REQUIRED',
)
}
- return { agent, ...openTurn(agent) }
+ return { agent, ...openTurnEvents(ctx, agent) }
+}
+
+/** Whether the captured open turn contains an event accepted by `predicate`. */
+function someOpenTurnEvent(
+ execution: GoalToolExecution,
+ predicate: (event: SessionEvent) => boolean,
+): boolean {
+ for (let seq = execution.openTurnStartSeq + 1; seq < execution.events.length; seq += 1) {
+ const event = execution.events[seq]
+ if (event !== undefined && predicate(event)) return true
+ }
+ return false
}
/**
@@ -69,13 +78,13 @@ export function goalToolExecution(ctx: Context, exec: ToolRunContext): GoalToolE
*/
function hasDirectHumanInput(ctx: Context, execution: GoalToolExecution): boolean {
if (!ctx.agents.roots().includes(execution.agent)) return false
- return execution.events.some(event =>
+ return someOpenTurnEvent(execution, event =>
event.type === 'user/message' && event.data.source.kind === 'user')
}
/** Whether this turn is the current goal's exact admitted round. */
function isMatchingGoalRound(execution: GoalToolExecution, goal: GoalView): boolean {
- return execution.events.some(event => event.type === 'user/message'
+ return someOpenTurnEvent(execution, event => event.type === 'user/message'
&& event.data.source.kind === 'goal'
&& event.data.source.goalId === goal.id
&& event.data.source.revision === goal.revision
diff --git a/packages/goal/tool-goal/src/index.ts b/packages/goal/tool-goal/src/index.ts
index ac13ea9213..4248d7cf0d 100644
--- a/packages/goal/tool-goal/src/index.ts
+++ b/packages/goal/tool-goal/src/index.ts
@@ -20,7 +20,7 @@ import {
import { renderWrapupContext } from './wrapup.ts'
export const name = 'tool-goal'
-export const inject = ['agents', 'goals', 'tools', 'systemPrompt']
+export const inject = ['agents', 'goals', 'tools', 'systemPrompt', 'sessionProjections']
/** Model policy and hard lower bounds for goal-state updates. */
export interface Config {
diff --git a/packages/goal/tool-goal/tests/tool-goal.spec.ts b/packages/goal/tool-goal/tests/tool-goal.spec.ts
index 5fc0215c0b..86765f87c8 100644
--- a/packages/goal/tool-goal/tests/tool-goal.spec.ts
+++ b/packages/goal/tool-goal/tests/tool-goal.spec.ts
@@ -3,6 +3,7 @@ import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
+import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
import GoalService, { GoalId } from '@deepseek-ai/dsh-goal'
import type { GoalRef } from '@deepseek-ai/dsh-goal'
import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
@@ -10,6 +11,7 @@ import type { MessageSource } from '@deepseek-ai/dsh-llm'
import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import * as toolGoal from '@deepseek-ai/dsh-tool-goal'
@@ -74,6 +76,8 @@ async function harness(config: toolGoal.Config = {}) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(AgentRegistry)
await ctx.plugin(ToolRuntime)
+ await ctx.plugin(SessionProjectionRegistry)
+ ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
await ctx.plugin(GoalService)
const fiber = await ctx.plugin(toolGoal, config)
const root = stubAgent(`goal-tool-root-${Math.random()}`)
@@ -164,7 +168,7 @@ describe('goal tool registration and presentation', () => {
it('has the Loader-safe namespace export shape', () => {
expect('default' in toolGoal).toBe(false)
expect(toolGoal.name).toBe('tool-goal')
- expect(toolGoal.inject).toEqual(['agents', 'goals', 'tools', 'systemPrompt'])
+ expect(toolGoal.inject).toEqual(['agents', 'goals', 'tools', 'systemPrompt', 'sessionProjections'])
const loader = Object.create(Loader.prototype) as Loader
expect(loader.unwrapExports(toolGoal)).toBe(toolGoal)
})
diff --git a/packages/goal/tool-goal/tsconfig.json b/packages/goal/tool-goal/tsconfig.json
index 1570cd7d2d..1933a0c8b3 100644
--- a/packages/goal/tool-goal/tsconfig.json
+++ b/packages/goal/tool-goal/tsconfig.json
@@ -37,6 +37,9 @@
},
{
"path": "../../runtime-diagnostics/invariants"
+ },
+ {
+ "path": "../../session/session-projection"
}
]
}
diff --git a/packages/guard/repeat-tool-reminder/package.json b/packages/guard/repeat-tool-reminder/package.json
index 9bc0631cd7..bf668f9651 100644
--- a/packages/guard/repeat-tool-reminder/package.json
+++ b/packages/guard/repeat-tool-reminder/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-repeat-tool-reminder",
"description": "Repeat-tool-call guard plugin: advisory reminders when an agent loops on identical tool calls",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -48,6 +48,7 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/guard/repeat-tool-reminder/tests/repeat-tool-reminder.spec.ts b/packages/guard/repeat-tool-reminder/tests/repeat-tool-reminder.spec.ts
index ea0f0ceef8..58369c16b7 100644
--- a/packages/guard/repeat-tool-reminder/tests/repeat-tool-reminder.spec.ts
+++ b/packages/guard/repeat-tool-reminder/tests/repeat-tool-reminder.spec.ts
@@ -6,6 +6,7 @@ import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as RepeatToolGuard from '@deepseek-ai/dsh-repeat-tool-reminder'
import type { Config } from '@deepseek-ai/dsh-repeat-tool-reminder'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -24,6 +25,8 @@ const testToolSignal = new AbortController().signal
async function harness(config: Config = {}): Promise {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ // AgentLoop declares the registry as a required injection.
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(RepeatToolGuard, config)
ctx.tools.register(defineContentToolFixture({ name: 'probe', description: 'p', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } }))
@@ -370,6 +373,7 @@ describe('config validation fails loud', () => {
async function spine(): Promise {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
return ctx
}
diff --git a/packages/guard/timeout-policy/package.json b/packages/guard/timeout-policy/package.json
index bef6cf1251..3424ff6c92 100644
--- a/packages/guard/timeout-policy/package.json
+++ b/packages/guard/timeout-policy/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-call-timeout-policy",
"description": "Tool-call timeout policy: a tools/execute wrapper that arms a per-tool deadline on exec.signal and returns TOOL_TIMEOUT when it wins",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/hooks/hook-protocol/package.json b/packages/hooks/hook-protocol/package.json
index 7d65ddb10a..f89330eaac 100644
--- a/packages/hooks/hook-protocol/package.json
+++ b/packages/hooks/hook-protocol/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-hook-protocol",
"description": "Shared Claude Code / Codex hook wire protocol: matcher engine, stdin/exit-code/stdout codec, multi-hook merge, and hook/* session events",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/hooks/hooks-claude-code/package.json b/packages/hooks/hooks-claude-code/package.json
index 958a94e4dd..c8bf01077b 100644
--- a/packages/hooks/hooks-claude-code/package.json
+++ b/packages/hooks/hooks-claude-code/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-hooks-claude-code",
"description": "Bridge plugin: run a Claude Code hooks.json / settings hook config on the DeepSeek Harness interception seams",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -43,7 +43,8 @@
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -60,6 +61,7 @@
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/hooks/hooks-claude-code/src/index.ts b/packages/hooks/hooks-claude-code/src/index.ts
index 09594d18b6..e82b728a8e 100644
--- a/packages/hooks/hooks-claude-code/src/index.ts
+++ b/packages/hooks/hooks-claude-code/src/index.ts
@@ -12,7 +12,8 @@
import { readFileSync } from 'node:fs'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
-import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
+import type { Agent, PreStepDecision, TurnBoundaryProjection } from '@deepseek-ai/dsh-agent'
+import type {} from '@deepseek-ai/dsh-session-projection'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-session'
@@ -37,9 +38,9 @@ import type { SubagentRunId } from '@deepseek-ai/dsh-subagent'
import { parseClaudeCodeConfig, type ClaudeCodeHookConfig } from './config.ts'
export const name = 'hooks-claude-code'
-// `bash` is required to run hooks; the rest are read opportunistically via
-// ctx.get so a deployment can load this bridge without every extension point present.
-export const inject = ['shell']
+// `shell` runs hooks and `sessionProjections` supplies turn numbers; the rest
+// are read opportunistically via ctx.get so a deployment can omit them.
+export const inject = ['shell', 'sessionProjections']
/** Plugin config: where the CC hook config lives + substitution roots. */
export interface Config {
@@ -236,7 +237,7 @@ export function apply(ctx: Context, config: Config): void {
// --- PreToolUse → PreToolDecision. Matcher subject is the tool name. ---
ctx.on('tools/pre-execute', async (exec, next): Promise => {
- const turn = lastTurn(exec.agent)
+ const turn = lastTurn(ctx, exec.agent)
const merged = await runPoint('PreToolUse', exec.name, preToolPayload(ctx, exec), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal })
if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' }
if (merged.decision === 'ask') return { kind: 'ask', ...merged.reason !== undefined ? { reason: merged.reason } : {} }
@@ -245,7 +246,7 @@ export function apply(ctx: Context, config: Config): void {
// --- PostToolUse → PostToolDecision. Matcher subject is the tool name. ---
ctx.on('tools/post-execute', async (exec, result, next): Promise => {
- const turn = lastTurn(exec.agent)
+ const turn = lastTurn(ctx, exec.agent)
const merged = await runPoint('PostToolUse', exec.name, postToolPayload(ctx, exec, result), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal })
const context = contextFrom(merged)
if (merged.decision === 'deny') {
@@ -307,11 +308,10 @@ const SUBAGENT_TYPE = 'general-purpose'
// hook input schema; this is the part a bridge owns. ---
/** The last open turn number in the agent's log, or 0 without an agent. */
-function lastTurn(agent: Agent | undefined): number {
+function lastTurn(ctx: Context, agent: Agent | undefined): number {
if (!agent) return 0
- const last = [...agent.session.events].findLast(e => e.type === 'turn/start')
- /* v8 ignore next -- agent-present callers are tool/stop extension points inside an open turn. */
- return last?.type === 'turn/start' ? last.data.turn : 0
+ const boundary = ctx.sessionProjections.stateOf(agent.session, 'turnBoundary') as TurnBoundaryProjection
+ return boundary.lastTurn
}
/** Flatten content blocks to the text a hook payload carries (the common case). */
diff --git a/packages/hooks/hooks-claude-code/tests/bridge.spec.ts b/packages/hooks/hooks-claude-code/tests/bridge.spec.ts
index 88a357b1aa..af323d9b63 100644
--- a/packages/hooks/hooks-claude-code/tests/bridge.spec.ts
+++ b/packages/hooks/hooks-claude-code/tests/bridge.spec.ts
@@ -15,6 +15,7 @@ import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import SubagentRuntime, { SubagentRunId } from '@deepseek-ai/dsh-subagent'
import * as HooksClaude from '@deepseek-ai/dsh-hooks-claude-code'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
/**
@@ -57,6 +58,7 @@ async function harnessWithFiber(
): Promise<{ ctx: Context; hooks: Fiber }> {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
@@ -354,6 +356,7 @@ describe('hooks-claude-code bridge — load resilience', () => {
const adapter = new MockAdapter([textResponse('fine')])
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
@@ -414,6 +417,7 @@ describe('hooks-claude-code bridge — load resilience', () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
@@ -434,12 +438,12 @@ describe('hooks-claude-code bridge — load resilience', () => {
// "cannot get property … without inject". Guard the shape directly.
expect('default' in HooksClaude).toBe(false)
expect(HooksClaude.name).toBe('hooks-claude-code')
- expect(HooksClaude.inject).toEqual(['shell'])
+ expect(HooksClaude.inject).toEqual(['shell', 'sessionProjections'])
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(HooksClaude) as Record
expect(unwrapped).toBe(HooksClaude)
expect(unwrapped.name).toBe('hooks-claude-code')
- expect(unwrapped.inject).toEqual(['shell'])
+ expect(unwrapped.inject).toEqual(['shell', 'sessionProjections'])
expect(typeof unwrapped.apply).toBe('function')
})
})
diff --git a/packages/hooks/hooks-claude-code/tests/coverage-cases.ts b/packages/hooks/hooks-claude-code/tests/coverage-cases.ts
index 416010d69f..4be08bb348 100644
--- a/packages/hooks/hooks-claude-code/tests/coverage-cases.ts
+++ b/packages/hooks/hooks-claude-code/tests/coverage-cases.ts
@@ -15,6 +15,7 @@ import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import SubagentRuntime, { SubagentRunId } from '@deepseek-ai/dsh-subagent'
import * as HooksClaude from '@deepseek-ai/dsh-hooks-claude-code'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
const testToolSignal = new AbortController().signal
@@ -41,6 +42,7 @@ type HarnessOpts = { pluginRoot?: string; projectDir?: string; stderrSummaryMaxC
async function harness(configPath: string, adapter: MockAdapter, opts: HarnessOpts = {}): Promise {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
if (opts.sessionRoot !== undefined) await ctx.plugin(JsonlSessionPersistence, { root: opts.sessionRoot })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
@@ -368,6 +370,7 @@ export function defineCoverageCases(group: CoverageGroup): void {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
@@ -667,6 +670,7 @@ export function defineCoverageCases(group: CoverageGroup): void {
const adapter = new MockAdapter([toolCallResponse('c1', 'echo', {}), textResponse('done')])
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
// Executor default cwd = serverDir (deliberately NOT the session cwd).
await ctx.plugin(LocalSubprocessRuntime)
@@ -696,6 +700,7 @@ export function defineCoverageCases(group: CoverageGroup): void {
hooks(serverDir, { SubagentStop: [{ hooks: [{ type: 'command', command: 'cat > stoppayload.tmp; mv stoppayload.tmp stoppayload; pwd > stopwhere' }] }] })
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
// Executor default cwd = serverDir (deliberately NOT the child session cwd).
await ctx.plugin(LocalSubprocessRuntime)
diff --git a/packages/hooks/hooks-codex/package.json b/packages/hooks/hooks-codex/package.json
index 1308ea38db..b267130054 100644
--- a/packages/hooks/hooks-codex/package.json
+++ b/packages/hooks/hooks-codex/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-hooks-codex",
"description": "Bridge plugin: run a Codex hooks.json hook config on the DeepSeek Harness interception seams",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -42,7 +42,8 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -58,6 +59,7 @@
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/hooks/hooks-codex/src/index.ts b/packages/hooks/hooks-codex/src/index.ts
index 2189fc9a28..b8f8027c88 100644
--- a/packages/hooks/hooks-codex/src/index.ts
+++ b/packages/hooks/hooks-codex/src/index.ts
@@ -16,6 +16,7 @@ import { readFileSync } from 'node:fs'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
+import type {} from '@deepseek-ai/dsh-session-projection'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-session'
@@ -38,7 +39,7 @@ import { parseCodexConfig, type CodexHookConfig } from './config.ts'
/* jscpd:ignore-end */
export const name = 'hooks-codex'
-export const inject = ['shell']
+export const inject = ['shell', 'sessionProjections']
/** Plugin config: where the Codex hooks.json lives + the model name for payloads. */
export interface Config {
@@ -223,7 +224,7 @@ export function apply(ctx: Context, config: Config): void {
// PreToolUse → PreToolDecision. Codex blocks only (no allow/ask honored).
ctx.on('tools/pre-execute', async (exec, next): Promise => {
- const turn = lastTurn(exec.agent)
+ const turn = lastTurn(ctx, exec.agent)
const merged = await runPoint('PreToolUse', exec.name, preToolPayload(ctx, exec, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal })
/* jscpd:ignore-end */
if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' }
@@ -232,7 +233,7 @@ export function apply(ctx: Context, config: Config): void {
// PostToolUse → PostToolDecision (block with feedback, or attach context).
ctx.on('tools/post-execute', async (exec, result, next): Promise => {
- const turn = lastTurn(exec.agent)
+ const turn = lastTurn(ctx, exec.agent)
/* jscpd:ignore-start */
const merged = await runPoint('PostToolUse', exec.name, postToolPayload(ctx, exec, result, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal })
const context = contextFrom(merged)
@@ -276,11 +277,10 @@ export function apply(ctx: Context, config: Config): void {
// These small payload helpers intentionally remain next to the dialect shape;
// sharing them would pull bridge-only agent/LLM dependencies into hook-protocol.
/* jscpd:ignore-start */
-function lastTurn(agent: Agent | undefined): number {
+function lastTurn(ctx: Context, agent: Agent | undefined): number {
if (!agent) return 0
- const last = [...agent.session.events].findLast(e => e.type === 'turn/start')
- /* v8 ignore next -- agent-present turnBase callers are tool/stop extension points inside an open turn. */
- return last?.type === 'turn/start' ? last.data.turn : 0
+ /* v8 ignore next -- agent-present hook points run inside AgentLoop, which owns this projection. */
+ return ctx.sessionProjections.stateOf(agent.session, 'turnBoundary')?.lastTurn ?? 0
}
function blocksToText(content: ContentBlock[]): string {
@@ -304,7 +304,7 @@ function base(ctx: Context, agent: Agent | undefined, event: string, model: stri
/** Base + turn_id, for the turn-scoped events (PreToolUse/PostToolUse/UserPromptSubmit/Stop). */
function turnBase(ctx: Context, agent: Agent | undefined, event: string, model: string): Record {
- return { ...base(ctx, agent, event, model), turn_id: String(lastTurn(agent)) }
+ return { ...base(ctx, agent, event, model), turn_id: String(lastTurn(ctx, agent)) }
}
/** Extract a `command` string from a tool call's parsed arguments, else ''. */
diff --git a/packages/hooks/hooks-codex/tests/bridge.spec.ts b/packages/hooks/hooks-codex/tests/bridge.spec.ts
index d35cfe7993..bac95b7eed 100644
--- a/packages/hooks/hooks-codex/tests/bridge.spec.ts
+++ b/packages/hooks/hooks-codex/tests/bridge.spec.ts
@@ -13,6 +13,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as HooksCodex from '@deepseek-ai/dsh-hooks-codex'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
/**
@@ -42,6 +43,7 @@ function writeHooks(dir: string, hooks: unknown): void {
async function harness(dir: string, adapter: MockAdapter, beforeHooks?: (ctx: Context) => void): Promise {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
@@ -182,6 +184,7 @@ describe('hooks-codex bridge', () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
@@ -205,6 +208,7 @@ describe('hooks-codex bridge', () => {
writeHooks(dir, { SessionStart: [{ hooks: [{ type: 'command', command: slow }] }] })
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
@@ -227,12 +231,12 @@ describe('hooks-codex bridge', () => {
it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/inject/apply', () => {
expect('default' in HooksCodex).toBe(false)
expect(HooksCodex.name).toBe('hooks-codex')
- expect(HooksCodex.inject).toEqual(['shell'])
+ expect(HooksCodex.inject).toEqual(['shell', 'sessionProjections'])
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(HooksCodex) as Record
expect(unwrapped).toBe(HooksCodex)
expect(unwrapped.name).toBe('hooks-codex')
- expect(unwrapped.inject).toEqual(['shell'])
+ expect(unwrapped.inject).toEqual(['shell', 'sessionProjections'])
expect(typeof unwrapped.apply).toBe('function')
})
})
diff --git a/packages/hooks/hooks-codex/tests/coverage-cases.ts b/packages/hooks/hooks-codex/tests/coverage-cases.ts
index dcb5cd4443..7fed07518a 100644
--- a/packages/hooks/hooks-codex/tests/coverage-cases.ts
+++ b/packages/hooks/hooks-codex/tests/coverage-cases.ts
@@ -13,6 +13,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as HooksCodex from '@deepseek-ai/dsh-hooks-codex'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
const testToolSignal = new AbortController().signal
@@ -31,6 +32,7 @@ type HarnessOpts = { stderrSummaryMaxChars?: number; sessionRoot?: string }
async function harness(configPath: string, adapter: MockAdapter, opts: HarnessOpts = {}): Promise {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
if (opts.sessionRoot !== undefined) await ctx.plugin(JsonlSessionPersistence, { root: opts.sessionRoot })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
@@ -314,6 +316,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
const adapter = new MockAdapter([textResponse('ok')])
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
@@ -624,6 +627,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
const adapter = new MockAdapter([toolCallResponse('c1', 'Bash', { command: 'x' }), textResponse('done')])
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000, cwd: serverDir })
diff --git a/packages/host/directory-picker-auto/package.json b/packages/host/directory-picker-auto/package.json
index 6693a4e6de..ffc7337a0b 100644
--- a/packages/host/directory-picker-auto/package.json
+++ b/packages/host/directory-picker-auto/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-host-directory-picker-auto",
"description": "Adaptive chooser of the directory-picker seam: resolves the host situation at boot and mounts the native or browse backend for the DeepSeek Harness web GUI host",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/host/directory-picker-auto/src/index.ts b/packages/host/directory-picker-auto/src/index.ts
index 0044bc3e28..8e41edef80 100644
--- a/packages/host/directory-picker-auto/src/index.ts
+++ b/packages/host/directory-picker-auto/src/index.ts
@@ -43,9 +43,8 @@ export const BACKEND_PACKAGES: Record = {
* Client surface package per resolved kind, mounted with its backend so one
* resolved interaction still composes both faces. Declared as dependencies by
* every composing app for the same reason as {@link BACKEND_PACKAGES}. Only the
- * specifier is referenced here — the packages belong to the Client program, so
- * no import of them exists on this side and knip needs them ignored for this
- * workspace.
+ * specifier is referenced here because the packages belong to the Client
+ * program, so no import of them exists on this side.
*/
export const SURFACE_PACKAGES: Record = {
native: '@deepseek-ai/dsh-client-ui-directory-picker-native',
diff --git a/packages/host/directory-picker-browse/package.json b/packages/host/directory-picker-browse/package.json
index 2936081351..48da08f3c8 100644
--- a/packages/host/directory-picker-browse/package.json
+++ b/packages/host/directory-picker-browse/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-host-directory-picker-browse",
"description": "In-app browsing backend of the directory-picker seam (listing/creation primitives over the host filesystem)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/host/directory-picker-native/package.json b/packages/host/directory-picker-native/package.json
index 8215d96874..341fb22e75 100644
--- a/packages/host/directory-picker-native/package.json
+++ b/packages/host/directory-picker-native/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-host-directory-picker-native",
"description": "Native-OS-chooser backend of the directory-picker seam for the DeepSeek Harness web GUI host",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/host/directory-picker/package.json b/packages/host/directory-picker/package.json
index 6fb6652bdc..d6fc2a352e 100644
--- a/packages/host/directory-picker/package.json
+++ b/packages/host/directory-picker/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-host-directory-picker",
"description": "Abstract workspace-directory picking seam (ctx.directoryPicker) for the DeepSeek Harness web GUI host",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/host/frontend-static/package.json b/packages/host/frontend-static/package.json
index 067548dca2..97161a2b70 100644
--- a/packages/host/frontend-static/package.json
+++ b/packages/host/frontend-static/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-host-frontend-static",
"description": "SPA dist server for the Web shell: owns the webserver fallback seat, serving explicit index entries and static assets with traversal rejection and 404 misses",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/host/plugin-inventory/package.json b/packages/host/plugin-inventory/package.json
index ebfcf857a6..e9db804471 100644
--- a/packages/host/plugin-inventory/package.json
+++ b/packages/host/plugin-inventory/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-host-plugin-inventory",
"description": "Read-only Remote projection of current Cordis Loader plugin state",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/host/webserver/package.json b/packages/host/webserver/package.json
index e3fc2bb6d9..5715d31f09 100644
--- a/packages/host/webserver/package.json
+++ b/packages/host/webserver/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-host-webserver",
"description": "Web route-registration plugin: HTTP and upgrade routes, index transform taps, and static dist fallback; knows no harness concepts",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/identity/anonymous-user-id/package.json b/packages/identity/anonymous-user-id/package.json
index 35019e6c7b..543541487f 100644
--- a/packages/identity/anonymous-user-id/package.json
+++ b/packages/identity/anonymous-user-id/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-anonymous-user-id",
"description": "Shared anonymous user identity for DeepSeek Harness telemetry and feedback correlation",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/interaction/commands/package.json b/packages/interaction/commands/package.json
index f5eebc76d1..4a6d6660b3 100644
--- a/packages/interaction/commands/package.json
+++ b/packages/interaction/commands/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-commands",
"description": "Plugin-owned human command registry for DeepSeek Harness UIs",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/interaction/permission-presets/README.i18n.yaml b/packages/interaction/permission-presets/README.i18n.yaml
index 53ee5a2669..4d52d7dee6 100644
--- a/packages/interaction/permission-presets/README.i18n.yaml
+++ b/packages/interaction/permission-presets/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/interaction/permission-presets/README.md
-README.md: a704b1107ea571a0ae40b62d7c1fffb97622b61d
-README.zh.md: f9dc05bbbda2f6de8587d88229c1fbca37fe4289
+README.md: fa2ea0f96c7dc3cd7b31932942e2eaba85e656b6
+README.zh.md: 76bd299214e8650a3a46ebd4e1504c1131fba678
diff --git a/packages/interaction/permission-presets/README.md b/packages/interaction/permission-presets/README.md
index a704b1107e..fa2ea0f96c 100644
--- a/packages/interaction/permission-presets/README.md
+++ b/packages/interaction/permission-presets/README.md
@@ -87,15 +87,15 @@ The observable behavior is covered in [Use this package](#use-this-package); thi
### Read side and `custom`
-`current(events)` folds the three whole-value knob events over the composition defaults (`ctx.shell.sandboxMode` and the approval config). A still-matching last selection wins shared-bundle ties; otherwise the first table match wins; otherwise the derived `CUSTOM_PRESET` is returned. The `permissions` projection unit applies the same fold one event at a time and serves the select to clients.
+`current(session)` reads the `permissions` projection, whose unit folds the three whole-value knob events over the composition defaults (`ctx.shell.sandboxMode` and the approval config). The host state also retains whether `session/end-seed` has occurred, so session pinning distinguishes an explicitly empty restored seed from a genuinely fresh session without rescanning the log. A still-matching last selection wins shared-bundle ties; otherwise the first table match wins; otherwise the derived `CUSTOM_PRESET` is returned. A missing registry or projection key fails explicitly.
### Session pinning and blank reuse
-Mounting pins every live and future session: a genuinely fresh session gains the default preset and both knob facts, while seeded or partially initialized sessions keep their effective knob values and gain only missing durable facts.
+Mounting pins every live and future session: a genuinely fresh session gains the default preset and both knob facts, while seeded or partially initialized sessions keep their effective knob values and gain only missing durable facts. The projection-owned seed marker makes this decision from the same incremental state as the knob values.
### Optional children
-The `permissions` projection unit registers only when a `ctx.sessionProjections` registry is composed; the `/permission` command registers only when a `ctx.commands` registry is composed. Headless assemblies without either registry stay unaffected.
+The `permissions` projection unit registers only when a `ctx.sessionProjections` registry is composed; the `/permission` command registers only when a `ctx.commands` registry is composed. Calls that derive the current preset or pin an initial selection require the projection and fail explicitly without its registry or key.
diff --git a/packages/interaction/permission-presets/README.zh.md b/packages/interaction/permission-presets/README.zh.md
index f9dc05bbbd..76bd299214 100644
--- a/packages/interaction/permission-presets/README.zh.md
+++ b/packages/interaction/permission-presets/README.zh.md
@@ -87,15 +87,15 @@ kind: "package-reference"
### 读取侧与 `custom`
-`current(events)` 在组合默认值(`ctx.shell.sandboxMode` 与审批配置)之上折叠三个全量值旋钮事件。仍匹配的最近选择在共享捆绑时胜出;否则表中第一个匹配项胜出;否则返回推导出的 `CUSTOM_PRESET`。`permissions` 投影单元逐事件应用同一折叠,并向客户端提供选择器。
+`current(session)` 读取 `permissions` 投影;该单元在组合默认值(`ctx.shell.sandboxMode` 与审批配置)之上折叠三个全量值旋钮事件。host 状态还会保留 `session/end-seed` 是否已经出现,使会话固定无需重扫日志即可区分显式为空的恢复 seed 与真正的新会话。仍匹配的最近选择在共享捆绑时胜出;否则表中第一个匹配项胜出;否则返回推导出的 `CUSTOM_PRESET`。注册表或投影 key 缺失时会显式失败。
### 会话固定与空白复用
-挂载时会固定所有存活与未来的会话:真正全新的会话获得默认预设与两个旋钮事实,而 seed 会话或部分初始化的会话保留其有效旋钮值,只补充缺失的持久事实。
+挂载时会固定所有存活与未来的会话:真正全新的会话获得默认预设与两个旋钮事实,而 seed 会话或部分初始化的会话保留其有效旋钮值,只补充缺失的持久事实。投影自有的 seed 标记让该判断与旋钮值共用同一份增量状态。
### 可选子功能
-`permissions` 投影单元仅在组合了 `ctx.sessionProjections` 注册表时注册;`/permission` 命令仅在组合了 `ctx.commands` 注册表时注册。两者都未组合的无头装配不受影响。
+`permissions` 投影单元仅在组合了 `ctx.sessionProjections` 注册表时注册;`/permission` 命令仅在组合了 `ctx.commands` 注册表时注册。派生当前预设或固定初始选择的调用要求该投影存在,缺少注册表或 key 时会显式失败。
diff --git a/packages/interaction/permission-presets/package.json b/packages/interaction/permission-presets/package.json
index a9e3313204..aff216895b 100644
--- a/packages/interaction/permission-presets/package.json
+++ b/packages/interaction/permission-presets/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-permission-presets",
"description": "User-facing permission presets (ctx.permissionPresets) for the DeepSeek Harness: one product-level Permissions select bundling the sandbox-mode and approval-policy knobs, written through to their own session events",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/interaction/permission-presets/src/index.ts b/packages/interaction/permission-presets/src/index.ts
index 2366ff13f9..b6025fd98b 100644
--- a/packages/interaction/permission-presets/src/index.ts
+++ b/packages/interaction/permission-presets/src/index.ts
@@ -5,7 +5,7 @@
* and replay keep reading their knob folds. The preset event preserves user
* intent when two presets share a bundle. The read side ships as the
* `permissions` session projection; the write side ships as the
- * `/permission` command — both optional children over the same service.
+ * `/permission` command.
*
* @module dsh-permission-presets
*/
@@ -15,22 +15,18 @@ import z from '@deepseek-ai/schemastery'
import { z as zod } from 'zod'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
-import { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
+import { SANDBOX_MODES, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
// Side-effect type import: declaration-merges `ctx.shell` (the capability fact
// `sandboxMode` this service reads), without a value dependency on the seam.
import type {} from '@deepseek-ai/dsh-shell'
import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
-import { APPROVAL_POLICIES, effectiveApprovalPolicy, setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
+import { APPROVAL_POLICIES, setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
-// Type-only: resolves ctx.sessionProjections / ctx.commands for the optional children.
+// Type-only: resolves the optional projection and command children.
import type {} from '@deepseek-ai/dsh-session-projection'
import type {} from '@deepseek-ai/dsh-commands'
import type { PermissionSelect, PresetOption } from './types.ts'
-// The `permissions` projection-key declaration lives in src/types.ts (its one
-// home); this re-export projects the type face onto the package root AND
-// keeps the module edge in the emitted index.d.ts, so aggregate programs
-// consuming the declarations still receive the SessionProjectionMap merge.
export type * from './types.ts'
declare module '@deepseek-ai/cordis' {
@@ -39,12 +35,19 @@ declare module '@deepseek-ai/cordis' {
}
}
+declare module '@deepseek-ai/dsh-session-projection/types' {
+ interface SessionProjectionStateMap {
+ /** Latest logged permission overrides and constructor-seed provenance. */
+ permissions: PermissionProjectionState
+ }
+}
+
declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
/**
* Records the selected preset as durable, log-only user intent. The knob
* events follow in the same turn and control execution; this event stays
- * out of the model transcript and lets {@link effectivePermissionPreset}
+ * out of the model transcript and lets the permission projection unit
* preserve a selection when bundles match.
*/
'permission/preset': { preset: string }
@@ -73,23 +76,8 @@ export const CUSTOM_PRESET = 'custom'
export const PERMISSION_SETTINGS_NAMESPACE = settingsNamespace('permission')
/**
- * Fold the last selected preset from the durable log; replay needs no catch-up
- * state.
- * @param events - session events in log order; other event types are ignored.
- * @returns the last selected preset, or undefined when none was recorded.
- */
-export function effectivePermissionPreset(events: readonly SessionEvent[]): string | undefined {
- for (let index = events.length - 1; index >= 0; index -= 1) {
- const event = events[index] as SessionEvent
- if (event.type === 'permission/preset') return event.data.preset
- }
- return undefined
-}
-
-/**
- * The projection unit's state: the last seen value of each knob event, null
- * before an override (composition defaults apply at view time). Plain JSON
- * (persisted-cache precondition).
+ * The projection unit's knob state: the last seen value of each knob event,
+ * null before an override (composition defaults apply at view time).
*/
export interface KnobState {
/** Last `permission/preset` payload, or null. */
@@ -100,13 +88,13 @@ export interface KnobState {
approval: ApprovalPolicy | null
}
-declare module '@deepseek-ai/dsh-session-projection/types' {
- interface SessionProjectionStateMap {
- permissions: KnobState
- }
+/** Projection state for permission overrides and constructor-seed provenance. */
+interface PermissionProjectionState extends KnobState {
+ /** Whether the log contains a constructor-seed boundary. */
+ seeded: boolean
}
-const knobStateSchema: zod.ZodType = zod.object({
+const permissionStateSchema: zod.ZodType = zod.object({
preset: zod.string().nullable(),
sandbox: zod.union([
zod.literal('read-only'),
@@ -114,19 +102,23 @@ const knobStateSchema: zod.ZodType = zod.object({
zod.literal('danger-full-access'),
]).nullable(),
approval: zod.union([zod.literal('ask'), zod.literal('never')]).nullable(),
+ seeded: zod.boolean(),
}).strict()
/** State for the empty log: every knob at its composition default. */
const EMPTY_KNOBS: KnobState = { preset: null, sandbox: null, approval: null }
/**
- * One-event knob transition (the projection unit's `apply`). Uninterested
+ * One-event permission-state transition (the projection unit's `apply`). Unrelated
* events return the same reference — the registry's change gate.
* @param state - the folded knob state before `event`.
* @param event - one committed session event.
- * @returns the next state; the same reference when the event is not a knob.
+ * @returns the next state; the same reference when the event is unrelated.
*/
-export function applyKnobEvent(state: KnobState, event: SessionEvent): KnobState {
+function applyPermissionEvent(
+ state: PermissionProjectionState,
+ event: SessionEvent,
+): PermissionProjectionState {
switch (event.type) {
case 'permission/preset':
return { ...state, preset: event.data.preset }
@@ -134,18 +126,13 @@ export function applyKnobEvent(state: KnobState, event: SessionEvent): KnobState
return { ...state, sandbox: event.data.mode }
case 'approval/policy':
return { ...state, approval: event.data.policy }
+ case 'session/end-seed':
+ return { ...state, seeded: true }
default:
return state
}
}
-/** Whole-log knob fold (the cold-read parallel of {@link applyKnobEvent}). */
-function foldKnobs(events: readonly SessionEvent[]): KnobState {
- let state = EMPTY_KNOBS
- for (const event of events) state = applyKnobEvent(state, event)
- return state
-}
-
/** User setting resolved when a new session receives its initial permission. */
export interface PermissionSettings {
/** Preset pinned into a newly created session. */
@@ -233,17 +220,6 @@ export class PermissionPresetService extends Service {
onChange: () => {},
})
- ctx.on('session/created', (session) => {
- this.pinInitialPermission(session)
- })
- for (const session of ctx.sessions.list()) {
- this.pinInitialPermission(session)
- }
-
- // The permissions projection unit: fold the three whole-value knob
- // events; view derives the select over the composition defaults this
- // service already owns. The unit child activates only when a projection
- // registry is composed (headless assemblies stay unaffected).
// zod `.optional()` types the key `string | undefined` while the domain
// says `description?: string`; on the JSON wire the two serialize
// identically (absent), so the cast records exactly that
@@ -256,15 +232,23 @@ export class PermissionPresetService extends Service {
})),
currentValue: zod.string().min(1),
}) as unknown as zod.ZodType
+ // The projection child activates only when a registry is composed. Service
+ // reads reject a missing registry or key instead of substituting defaults.
ctx.inject(['sessionProjections'], (projectionCtx) => {
- projectionCtx.sessionProjections.register<'permissions', KnobState>({
+ projectionCtx.sessionProjections.register({
key: 'permissions',
- stateSchema: knobStateSchema,
- init: () => EMPTY_KNOBS,
- apply: applyKnobEvent,
+ stateVersion: 2,
+ stateSchema: permissionStateSchema,
+ init: () => ({ ...EMPTY_KNOBS, seeded: false }),
+ apply: applyPermissionEvent,
wire: { viewSchema: selectSchema, view: state => this.selectFor(state) },
- stateVersion: 1,
})
+ projectionCtx.on('session/created', (session) => {
+ this.pinInitialPermission(session)
+ })
+ for (const session of projectionCtx.sessions.list()) {
+ this.pinInitialPermission(session)
+ }
})
// The /permission command: the one write path a web client uses (the
@@ -281,7 +265,7 @@ export class PermissionPresetService extends Service {
handler: ({ agent, rawInput }) => {
const name = rawInput.trim()
if (name === '') {
- return { kind: 'success', text: `current preset ${this.current(agent.session.events)} (available: ${this.names.join(', ')})` }
+ return { kind: 'success', text: `current preset ${this.current(agent.session)} (available: ${this.names.join(', ')})` }
}
if (!this.names.includes(name)) {
return { kind: 'error', text: `unknown preset "${name}" (available: ${this.names.join(', ')})` }
@@ -310,15 +294,23 @@ export class PermissionPresetService extends Service {
return this.defaultSettings().defaultPreset
}
+ private permissionState(session: Session): PermissionProjectionState {
+ const projections = this.ctx.get('sessionProjections')
+ if (projections === undefined) throw new Error('permission: session projection registry is unavailable')
+ const state = projections.stateOf(session, 'permissions')
+ if (state === undefined) throw new Error('permission: permissions session projection is not registered')
+ return state
+ }
+
/**
* 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.
+ * @param session - the session whose knob state is read.
* @returns the effective preset name, or `custom` when nothing matches.
*/
- current(events: readonly SessionEvent[]): string {
- return this.derive(foldKnobs(events))
+ current(session: Session): string {
+ return this.derive(this.permissionState(session))
}
/** Resolve the preset for one folded knob state (the shared mathematics of `current` and the projection unit). */
@@ -395,14 +387,14 @@ export class PermissionPresetService extends Service {
/** Apply one preset with the caller-selected live or initialization policy writer. */
private apply(session: Session, name: string, setApproval: (policy: ApprovalPolicy) => void): void {
const spec = this.resolve(name)
- if (this.current(session.events) !== name) {
+ if (this.current(session) !== name) {
session.append('permission/preset', { preset: name })
}
- const events = session.events
- if (spec.sandbox !== (effectiveSandboxMode(events) ?? this.ctx.shell.sandboxMode)) {
+ const knobs = this.permissionState(session)
+ if (spec.sandbox !== (knobs.sandbox ?? this.ctx.shell.sandboxMode)) {
setSandboxMode(session, spec.sandbox)
}
- if (spec.approval !== (effectiveApprovalPolicy(events) ?? this.ctx.approval.config.policy ?? 'ask')) {
+ if (spec.approval !== (knobs.approval ?? this.ctx.approval.config.policy ?? 'ask')) {
setApproval(spec.approval)
}
}
@@ -414,12 +406,12 @@ export class PermissionPresetService extends Service {
* the missing durable facts.
*/
private pinInitialPermission(session: Session): void {
- const events = session.events
- const selected = effectivePermissionPreset(events)
- const sandbox = effectiveSandboxMode(events)
- const approval = effectiveApprovalPolicy(events)
- const seeded = events.some(event => event.type === 'session/end-seed')
- if (selected === undefined && sandbox === undefined && approval === undefined && !seeded) {
+ const state = this.permissionState(session)
+ const selected = state.preset
+ const sandbox = state.sandbox
+ const approval = state.approval
+ const seeded = state.seeded
+ if (selected === null && sandbox === null && approval === null && !seeded) {
const name = this.defaultPreset
const spec = this.resolve(name)
session.append('permission/preset', { preset: name })
@@ -428,19 +420,14 @@ export class PermissionPresetService extends Service {
return
}
- const state: KnobState = {
- preset: selected ?? null,
- sandbox: sandbox ?? null,
- approval: approval ?? null,
- }
const effective = this.derive(state)
- if (selected === undefined && effective !== CUSTOM_PRESET) {
+ if (selected === null && effective !== CUSTOM_PRESET) {
session.append('permission/preset', { preset: effective })
}
- if (sandbox === undefined) {
+ if (sandbox === null) {
setSandboxMode(session, this.ctx.shell.sandboxMode as SandboxMode)
}
- if (approval === undefined) {
+ if (approval === null) {
setApprovalPolicy(session, this.ctx.approval.config.policy ?? 'ask')
}
}
diff --git a/packages/interaction/permission-presets/tests/permission-presets.spec.ts b/packages/interaction/permission-presets/tests/permission-presets.spec.ts
index 1f0e8760b8..4ef60e4c33 100644
--- a/packages/interaction/permission-presets/tests/permission-presets.spec.ts
+++ b/packages/interaction/permission-presets/tests/permission-presets.spec.ts
@@ -1,10 +1,11 @@
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import PermissionPresetService, {
- CUSTOM_PRESET, effectivePermissionPreset, PERMISSION_SETTINGS_NAMESPACE,
+ CUSTOM_PRESET, PERMISSION_SETTINGS_NAMESPACE,
} from '@deepseek-ai/dsh-permission-presets'
import type { Config } from '@deepseek-ai/dsh-permission-presets'
import { SettingsProvider } from '@deepseek-ai/dsh-settings'
@@ -29,9 +30,11 @@ async function mounted(options: {
config?: Config
bashDefault?: SandboxMode | undefined
approvalDefault?: ApprovalPolicy | undefined
+ projection?: boolean
} = {}): Promise {
const ctx = new Context()
await ctx.plugin(SessionStore)
+ if (options.projection !== false) await ctx.plugin(SessionProjectionRegistry)
ctx.provide('shell', {
sandboxMode: 'bashDefault' in options ? options.bashDefault : 'workspace-write',
resolve() { throw new Error('permission tests do not execute bash') },
@@ -50,6 +53,7 @@ function freshSession(id: string): Session {
async function mountedStore(options: { approvalDefault?: ApprovalPolicy | undefined } = {}): Promise {
const ctx = new Context()
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(MemorySettings)
ctx.provide('shell', {
sandboxMode: 'workspace-write',
@@ -64,20 +68,38 @@ async function mountedStore(options: { approvalDefault?: ApprovalPolicy | undefi
return ctx
}
-describe('effectivePermissionPreset', () => {
- it('folds to the last event, or undefined without one', () => {
+describe('permission preset fold', () => {
+ it('folds the latest preset selection and steps over unrelated events', async () => {
+ const ctx = await mounted()
const session = freshSession('sess-fold')
- expect(effectivePermissionPreset(session.events)).toBeUndefined()
+ const presetOf = () => ctx.sessionProjections.stateOf(session, 'permissions')?.preset ?? null
+ expect(presetOf()).toBeNull()
session.append('permission/preset', { preset: 'danger-full-access' })
session.append('permission/preset', { preset: 'workspace-write' })
- expect(effectivePermissionPreset(session.events)).toBe('workspace-write')
- // The backward scan steps over non-preset events to the latest selection.
+ expect(presetOf()).toBe('workspace-write')
+ // The knob fold steps over non-preset events to the latest selection.
session.append('sandbox/mode', { mode: 'read-only' })
- expect(effectivePermissionPreset(session.events)).toBe('workspace-write')
+ expect(presetOf()).toBe('workspace-write')
+
+ const seeded = Session.create(SessionId('sess-fold-seeded'), [])
+ expect(ctx.sessionProjections.stateOf(seeded, 'permissions')?.seeded).toBe(true)
})
})
describe('PermissionPresetService', () => {
+ it('fails on first state access when the projection registry is absent', async () => {
+ const ctx = await mounted({ projection: false })
+ expect(() => ctx.permissionPresets.current(freshSession('missing-permission-projections')))
+ .toThrow('permission: session projection registry is unavailable')
+ })
+
+ it('fails when the permissions projection key is absent', async () => {
+ const ctx = await mounted()
+ vi.spyOn(ctx.sessionProjections, 'stateOf').mockReturnValue(undefined)
+ expect(() => ctx.permissionPresets.current(freshSession('missing-permission-projection-key')))
+ .toThrow('permission: permissions session projection is not registered')
+ })
+
it('advertises the preset table in declaration order and resolves bundles', async () => {
const ctx = await mounted()
expect(ctx.permissionPresets.names).toEqual(['workspace-write', 'danger-full-access'])
@@ -88,18 +110,18 @@ describe('PermissionPresetService', () => {
it('current() derives from the effective knobs: composition defaults hit workspace-write, a switch hits its preset', async () => {
const ctx = await mounted()
const session = freshSession('sess-current')
- expect(ctx.permissionPresets.current(session.events)).toBe('workspace-write')
+ expect(ctx.permissionPresets.current(session)).toBe('workspace-write')
ctx.permissionPresets.set(session, 'danger-full-access')
- expect(ctx.permissionPresets.current(session.events)).toBe('danger-full-access')
+ expect(ctx.permissionPresets.current(session)).toBe('danger-full-access')
})
it('a knob state matching no table entry derives custom — a state, not an error', async () => {
const ctx = await mounted()
const session = freshSession('sess-custom')
session.append('sandbox/mode', { mode: 'read-only' })
- expect(ctx.permissionPresets.current(session.events)).toBe(CUSTOM_PRESET)
+ expect(ctx.permissionPresets.current(session)).toBe(CUSTOM_PRESET)
ctx.permissionPresets.set(session, 'danger-full-access')
- expect(ctx.permissionPresets.current(session.events)).toBe('danger-full-access')
+ expect(ctx.permissionPresets.current(session)).toBe('danger-full-access')
expect(() => ctx.permissionPresets.resolve(CUSTOM_PRESET)).toThrow(/unknown preset/)
})
@@ -109,7 +131,7 @@ describe('PermissionPresetService', () => {
config: { defaultPreset: 'workspace-write' },
})
const session = freshSession('sess-defaults-custom')
- expect(ctx.permissionPresets.current(session.events)).toBe(CUSTOM_PRESET)
+ expect(ctx.permissionPresets.current(session)).toBe(CUSTOM_PRESET)
})
it('the fold breaks bundle ties; a stale fold no longer matching falls back to table order', async () => {
@@ -120,10 +142,10 @@ describe('PermissionPresetService', () => {
} } })
const session = freshSession('sess-tie')
ctx.permissionPresets.set(session, 'agentish')
- expect(ctx.permissionPresets.current(session.events)).toBe('agentish')
+ expect(ctx.permissionPresets.current(session)).toBe('agentish')
session.append('approval/policy', { policy: 'never' })
session.append('sandbox/mode', { mode: 'danger-full-access' })
- expect(ctx.permissionPresets.current(session.events)).toBe('danger-full-access')
+ expect(ctx.permissionPresets.current(session)).toBe('danger-full-access')
})
it('set() writes through: one preset event plus both knob events', async () => {
@@ -188,7 +210,7 @@ describe('PermissionPresetService', () => {
const session = freshSession('sess-standin')
ctx.permissionPresets.set(session, 'workspace-write')
expect(session.events).toHaveLength(0)
- expect(ctx.permissionPresets.current(session.events)).toBe('workspace-write')
+ expect(ctx.permissionPresets.current(session)).toBe('workspace-write')
})
})
@@ -207,8 +229,8 @@ describe('new-session default', () => {
})
expect(ctx.permissionPresets.defaultPreset).toBe('danger-full-access')
const second = ctx.sessions.create(SessionId('second'))
- expect(ctx.permissionPresets.current(first.events)).toBe('workspace-write')
- expect(ctx.permissionPresets.current(second.events)).toBe('danger-full-access')
+ expect(ctx.permissionPresets.current(first)).toBe('workspace-write')
+ expect(ctx.permissionPresets.current(second)).toBe('danger-full-access')
expect(second.events.map(event => event.type)).toEqual([
'permission/preset', 'sandbox/mode', 'approval/policy',
])
@@ -223,7 +245,7 @@ describe('new-session default', () => {
legacy.append('turn/start', { turn: 1 })
legacy.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
const resumed = ctx.sessions.create(SessionId('legacy-resumed'), { seed: legacy.events })
- expect(ctx.permissionPresets.current(resumed.events)).toBe('workspace-write')
+ expect(ctx.permissionPresets.current(resumed)).toBe('workspace-write')
expect(resumed.events.slice(-3).map(event => event.type)).toEqual([
'permission/preset', 'sandbox/mode', 'approval/policy',
])
@@ -235,7 +257,7 @@ describe('new-session default', () => {
defaultPreset: 'danger-full-access',
})
const resumed = ctx.sessions.create(SessionId('empty-resumed'), { seed: [] })
- expect(ctx.permissionPresets.current(resumed.events)).toBe('workspace-write')
+ expect(ctx.permissionPresets.current(resumed)).toBe('workspace-write')
expect(resumed.events.map(event => event.type)).toEqual([
'session/end-seed', 'permission/preset', 'sandbox/mode', 'approval/policy',
])
@@ -244,6 +266,7 @@ describe('new-session default', () => {
it('pins sessions that already exist when the service remounts', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
ctx.provide('shell', {
sandboxMode: 'workspace-write',
resolve() { throw new Error('permission tests do not execute bash') },
@@ -258,7 +281,32 @@ describe('new-session default', () => {
expect(existing.events.map(event => event.type)).toEqual([
'permission/preset', 'sandbox/mode', 'approval/policy',
])
- expect(ctx.permissionPresets.current(existing.events)).toBe('workspace-write')
+ expect(ctx.permissionPresets.current(existing)).toBe('workspace-write')
+ })
+
+ it('preserves existing knob overrides when the service remounts over a knob-bearing session', async () => {
+ const ctx = new Context()
+ await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
+ ctx.provide('shell', {
+ sandboxMode: 'workspace-write',
+ resolve() { throw new Error('permission tests do not execute bash') },
+ run() { throw new Error('permission tests do not execute bash') },
+ start() { throw new Error('permission tests do not execute bash') },
+ })
+ ctx.provide('approval', { config: { policy: 'ask' } })
+ const existing = ctx.sessions.create(SessionId('existing-knobs'))
+ existing.append('sandbox/mode', { mode: 'read-only' })
+ existing.append('approval/policy', { policy: 'never' })
+
+ await ctx.plugin(PermissionPresetService, {})
+ // The remount sweep must read the folded knob events instead of treating
+ // the session as fresh; no default preset events may overwrite the
+ // overrides (read-only + never matches no preset table entry).
+ expect(existing.events.map(event => event.type)).toEqual([
+ 'sandbox/mode', 'approval/policy',
+ ])
+ expect(ctx.permissionPresets.current(existing)).toBe(CUSTOM_PRESET)
})
it('fills only missing legacy facts and preserves an unmatched seeded combination', async () => {
@@ -276,7 +324,7 @@ describe('new-session default', () => {
custom.append('sandbox/mode', { mode: 'read-only' })
custom.append('approval/policy', { policy: 'never' })
const unmatched = ctx.sessions.create(SessionId('custom-resumed'), { seed: custom.events })
- expect(ctx.permissionPresets.current(unmatched.events)).toBe(CUSTOM_PRESET)
+ expect(ctx.permissionPresets.current(unmatched)).toBe(CUSTOM_PRESET)
expect(unmatched.events.at(-1)?.type).toBe('session/end-seed')
})
diff --git a/packages/interaction/permission-presets/tests/projection.spec.ts b/packages/interaction/permission-presets/tests/projection.spec.ts
index f9bba33958..f382d4033d 100644
--- a/packages/interaction/permission-presets/tests/projection.spec.ts
+++ b/packages/interaction/permission-presets/tests/projection.spec.ts
@@ -2,8 +2,8 @@
* The `permissions` projection unit and the `/permission` command: mounting
* the permission service beside the projection registry serves the whole
* select (table options + effective current value, `custom` appended exactly
- * while derived) folded from the three knob events over the composition
- * defaults; the command child registers `/permission` whose handler switches
+ * while derived) folded from permission events over the composition defaults;
+ * the command child registers `/permission` whose handler switches
* through `permission.set` (bare invocation reports, unknown names error);
* compositions without either registry are unaffected; unmounting the
* service removes the key (HMR safety).
@@ -60,10 +60,9 @@ describe('permissions projection unit', () => {
changes.push({ key, value, seq })
})
ctx.permissionPresets.set(session, 'danger-full-access')
- // set() appends preset + sandbox/mode + approval/policy: three knob transitions.
- expect(changes).toHaveLength(3)
- expect(changes.at(-1)).toMatchObject({ key: 'permissions', value: { currentValue: 'danger-full-access' } })
- // Unrelated event: same-reference apply, no notification.
+ const permissionChanges = changes.filter(change => change.key === 'permissions')
+ expect(permissionChanges).toHaveLength(3)
+ expect(permissionChanges.at(-1)).toMatchObject({ key: 'permissions', value: { currentValue: 'danger-full-access' } })
session.append('turn/start', { turn: 1 })
expect(changes).toHaveLength(3)
})
@@ -80,7 +79,8 @@ describe('permissions projection unit', () => {
const { ctx, session } = await harness({ withPermission: false })
expect('permissions' in ctx.sessionProjections.snapshot(session).values).toBe(false)
const fiber = await ctx.plugin(PermissionPresetService, {})
- expect(ctx.sessionProjections.snapshot(session).values.permissions).toMatchObject({ currentValue: 'workspace-write' })
+ expect(ctx.sessionProjections.snapshot(session).values.permissions)
+ .toMatchObject({ currentValue: 'workspace-write' })
await fiber.dispose()
expect('permissions' in ctx.sessionProjections.snapshot(session).values).toBe(false)
})
@@ -92,7 +92,7 @@ describe('/permission command', () => {
const { agent, inject } = await agentFor(ctx, session)
const execution = await ctx.commands.execute(agent, '/permission danger-full-access', [], new AbortController().signal)
expect(execution?.result).toEqual({ kind: 'success', text: 'preset danger-full-access' })
- expect(ctx.permissionPresets.current(session.events)).toBe('danger-full-access')
+ expect(ctx.permissionPresets.current(session)).toBe('danger-full-access')
expect(inject.mock.calls[0]?.[0]).toMatchObject({
content: [{
type: 'text',
diff --git a/packages/interaction/tool-ask-user/package.json b/packages/interaction/tool-ask-user/package.json
index 067115a5d2..4bdeb65940 100644
--- a/packages/interaction/tool-ask-user/package.json
+++ b/packages/interaction/tool-ask-user/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-ask-user",
"description": "Model-facing ask_user_question tool over the ctx.userQuestions seam",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/interaction/user-approval/package.json b/packages/interaction/user-approval/package.json
index 5ec59a32d6..b414c6f567 100644
--- a/packages/interaction/user-approval/package.json
+++ b/packages/interaction/user-approval/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-user-approval",
"description": "User-approval seam (ctx.approval) for the DeepSeek Harness: one-shot permission decisions dispatched to composed answerers over the approval/request waterfall, fail-closed by default",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/interaction/user-questions/package.json b/packages/interaction/user-questions/package.json
index 21b01aeae3..e2313a3a97 100644
--- a/packages/interaction/user-questions/package.json
+++ b/packages/interaction/user-questions/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-user-questions",
"description": "Abstract user-questions seam (ctx.userQuestions) for asking the human during agent runs",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/jobs/jobs-local/package.json b/packages/jobs/jobs-local/package.json
index 117f9d9e99..02b00e36e1 100644
--- a/packages/jobs/jobs-local/package.json
+++ b/packages/jobs/jobs-local/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-jobs-local",
"description": "Process-local implementation of the DeepSeek Harness background job registry seam",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/jobs/jobs/package.json b/packages/jobs/jobs/package.json
index c658d2d49f..fec602b584 100644
--- a/packages/jobs/jobs/package.json
+++ b/packages/jobs/jobs/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-jobs",
"description": "Background job registry (ctx.jobs) for the DeepSeek Harness — shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/jobs/tool-jobs/package.json b/packages/jobs/tool-jobs/package.json
index fc1fc6877c..07cee3a02b 100644
--- a/packages/jobs/tool-jobs/package.json
+++ b/packages/jobs/tool-jobs/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-jobs",
"description": "Model-facing background job control tools (job_output, job_list, job_kill) over the ctx.jobs registry",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/llm/deepseek-llm-api-extensions/package.json b/packages/llm/deepseek-llm-api-extensions/package.json
index b82d01328d..a7a5c17eda 100644
--- a/packages/llm/deepseek-llm-api-extensions/package.json
+++ b/packages/llm/deepseek-llm-api-extensions/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-deepseek-llm-api-extensions",
"description": "Additive request-field registry for the official DeepSeek LLM API adapter",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json
index 49f42e6539..ebddd5b4f5 100644
--- a/packages/llm/llm-deepseek/package.json
+++ b/packages/llm/llm-deepseek/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-llm-deepseek",
"description": "DeepSeek chat-completions adapter for the DeepSeek Harness LLM seam",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json
index ce809946be..c8c3963f28 100644
--- a/packages/llm/llm-pi-ai/package.json
+++ b/packages/llm/llm-pi-ai/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-llm-pi-ai",
"description": "pi-ai-backed DeepSeek adapter for the DeepSeek Harness LLM seam (design-verification twin of dsh-llm-deepseek)",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/llm/llm-retry/package.json b/packages/llm/llm-retry/package.json
index e66c909078..9f6f59afb5 100644
--- a/packages/llm/llm-retry/package.json
+++ b/packages/llm/llm-retry/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-llm-retry",
"description": "Provider-routed LLM request retry policy for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -42,10 +42,12 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
},
"dependencies": {
- "@deepseek-ai/schemastery": "workspace:^"
+ "@deepseek-ai/schemastery": "workspace:^",
+ "zod": "^4.4.3"
},
"devDependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
@@ -64,6 +66,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^"
}
}
diff --git a/packages/llm/llm-retry/src/index.ts b/packages/llm/llm-retry/src/index.ts
index 45db304059..89ffc14c40 100644
--- a/packages/llm/llm-retry/src/index.ts
+++ b/packages/llm/llm-retry/src/index.ts
@@ -8,9 +8,10 @@
import { randomUUID } from 'node:crypto'
import type { Context, Events } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
+import { z as zod } from 'zod'
import type { Agent, RequestErrorAction } from '@deepseek-ai/dsh-agent'
import type { LlmFailure, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
-import type { SessionEvent } from '@deepseek-ai/dsh-session'
+import type {} from '@deepseek-ai/dsh-session-projection'
import { RetryId } from './brand.ts'
import type { LlmRetryEventData } from './types.ts'
@@ -18,7 +19,7 @@ export type { LlmRetryEventData, LlmRetryStartedEventData } from './types.ts'
export { RetryId } from './brand.ts'
export const name = 'llm-retry'
-export const inject = ['agents']
+export const inject = ['agents', 'sessionProjections']
/** This policy executor has no config; providers own `retryPolicy`. */
export type Config = Readonly>
@@ -75,6 +76,10 @@ function retryPolicyKey(policy: ResolvedRetryPolicy): string {
])
}
+function retryStateKey(provider: string, policyKey: string): string {
+ return JSON.stringify([provider, policyKey])
+}
+
function cancellableDelay(delayMs: number, signal: AbortSignal): Promise {
if (signal.aborted) return Promise.resolve(false)
return new Promise((resolve) => {
@@ -96,8 +101,41 @@ function cancellableDelay(delayMs: number, signal: AbortSignal): Promise
+
+// The cast bridges the branded retry id, which Zod cannot express directly.
+const llmRetryStateSchema: zod.ZodType = zod.record(zod.string(), zod.object({
+ retry: zod.number().int().nonnegative(),
+ retryId: zod.string(),
+})) as unknown as zod.ZodType
+declare module '@deepseek-ai/dsh-session-projection/types' {
+ interface SessionProjectionStateMap {
+ /** Retry state for the current step by provider and policy. */
+ llmRetry: LlmRetryState
+ }
+}
+
export function apply(ctx: Context, config: Config = {}, internals: RetryInternals = {}): void {
validateConfig(config)
+ ctx.sessionProjections.register({
+ key: 'llmRetry',
+ stateVersion: 1,
+ stateSchema: llmRetryStateSchema,
+ init: () => ({}),
+ apply: (state, event) => {
+ if (event.type === 'step/start' || event.type === 'turn/end') return {}
+ if (event.type !== 'llm/retry') return state
+ const key = retryStateKey(event.data.provider, event.data.policyKey)
+ const entry = state[key]
+ if (entry?.retry === event.data.retry && entry.retryId === event.data.retryId) return state
+ return { ...state, [key]: { retry: event.data.retry, retryId: event.data.retryId } }
+ },
+ })
const random = internals.random ?? Math.random
const lifetime = new AbortController()
const active = new Set>()
@@ -179,17 +217,12 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna
}
const policyKey = retryPolicyKey(policy)
- const priorPolicyRetry = agent.session.events.findLast((event): event is SessionEvent<'llm/retry'> =>
- event.type === 'llm/retry'
- && event.data.turn === turn
- && event.data.step === step
- && event.data.provider === provider
- && event.data.policyKey === policyKey,
- )
- const previousRetry = priorPolicyRetry?.data.retry ?? 0
+ const retryState = ctx.sessionProjections.stateOf(agent.session, 'llmRetry') as LlmRetryState
+ const previous = retryState[retryStateKey(provider, policyKey)]
+ const previousRetry = previous?.retry ?? 0
if (policy.mode === 'normal' && previousRetry >= policy.maxRetries) return next()
const retry = previousRetry + 1
- const retryId = priorPolicyRetry?.data.retryId ?? RetryId(randomUUID())
+ const retryId = previous?.retryId ?? RetryId(randomUUID())
let delayMs: number
if (failure.providerRetryAfterMs !== undefined
&& Number.isFinite(failure.providerRetryAfterMs)
diff --git a/packages/llm/llm-retry/tests/invariant.spec.ts b/packages/llm/llm-retry/tests/invariant.spec.ts
index 47d6d2a812..0c6c9c6b6b 100644
--- a/packages/llm/llm-retry/tests/invariant.spec.ts
+++ b/packages/llm/llm-retry/tests/invariant.spec.ts
@@ -4,7 +4,10 @@ import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import { createUserMessage, ProviderRequestId } from '@deepseek-ai/dsh-llm'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
import * as RetryInvariant from '@deepseek-ai/dsh-llm-retry/invariant'
+import * as retry from '../src/index.ts'
import { RetryId } from '@deepseek-ai/dsh-llm-retry'
import { providerForOpenStep } from '../src/history.ts'
@@ -58,6 +61,28 @@ const always = {
failure,
}
+describe('llm-retry projection fold', () => {
+ it('keeps one retry count per dispatch key across duplicate records', async () => {
+ const ctx = new Context()
+ await ctx.plugin(SessionStore)
+ await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(retry)
+ const session = ctx.sessions.create(SessionId('retry-dup'))
+ session.append('llm/retry', { turn: 1, step: 1, ...normal })
+ const first = ctx.sessionProjections.stateOf(session, 'llmRetry')
+ session.append('llm/retry', { turn: 1, step: 1, ...normal })
+ const second = ctx.sessionProjections.stateOf(session, 'llmRetry')
+ expect(second).toBe(first)
+ expect(second).toEqual({
+ '["mock","normal-policy"]': {
+ retry: 1,
+ retryId: 'normal-retry-chain',
+ },
+ })
+ })
+})
+
describe('llm-retry invariants', () => {
it('has no provider without the requested open step or a route marker', () => {
expect(providerForOpenStep([], 1, 1)).toBeUndefined()
diff --git a/packages/llm/llm-retry/tests/loader-composition.spec.ts b/packages/llm/llm-retry/tests/loader-composition.spec.ts
index 367e8f46f4..b4bef4c239 100644
--- a/packages/llm/llm-retry/tests/loader-composition.spec.ts
+++ b/packages/llm/llm-retry/tests/loader-composition.spec.ts
@@ -11,6 +11,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmRuntime, { createUserMessage, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import * as retry from '../src/index.ts'
@@ -60,6 +61,7 @@ async function loadYaml(lines: readonly string[]): Promise {
const modules = new Map([
['@deepseek-ai/dsh-llm', LlmRuntime],
['@deepseek-ai/dsh-session', SessionStore],
+ ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
['@deepseek-ai/dsh-system-prompt', SystemPrompt],
['@deepseek-ai/dsh-tools', ToolRuntime],
['@deepseek-ai/dsh-agent', AgentRegistry],
@@ -89,6 +91,7 @@ describe('real Loader composition', () => {
const loaded = await loadYaml([
"- name: '@deepseek-ai/dsh-llm'",
"- name: '@deepseek-ai/dsh-session'",
+ "- name: '@deepseek-ai/dsh-session-projection'",
"- name: '@deepseek-ai/dsh-system-prompt'",
"- name: '@deepseek-ai/dsh-tools'",
"- name: '@deepseek-ai/dsh-agent'",
diff --git a/packages/llm/llm-retry/tests/retry.spec.ts b/packages/llm/llm-retry/tests/retry.spec.ts
index bc1ed6bab6..30ffd662e9 100644
--- a/packages/llm/llm-retry/tests/retry.spec.ts
+++ b/packages/llm/llm-retry/tests/retry.spec.ts
@@ -12,6 +12,7 @@ import type {
StreamChunk,
} from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session'
import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
@@ -107,6 +108,7 @@ async function harness(
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1025,14 +1027,20 @@ describe('provider-routed retry policy', () => {
it('rejects retry policy configured on the executor instead of a provider', () => {
expectTypeOf<{}>().toExtend()
expectTypeOf<{ retryPolicy: { mode: 'always' } }>().not.toExtend()
+ const ctx = new Context()
+ // `apply` registers its projection unit first; the registry is a required
+ // injection, so the direct-apply path must carry it too.
+ new SessionProjectionRegistry(ctx)
expect(() => {
- retry.apply(new Context(), { retryPolicy: { mode: 'always' } } as unknown as retry.Config)
+ retry.apply(ctx, { retryPolicy: { mode: 'always' } } as unknown as retry.Config)
}).toThrow(/retryPolicy belongs under each provider/)
})
it('rejects unknown executor config', () => {
+ const ctx = new Context()
+ new SessionProjectionRegistry(ctx)
expect(() => {
- retry.apply(new Context(), { retryPolciy: {} } as unknown as retry.Config)
+ retry.apply(ctx, { retryPolciy: {} } as unknown as retry.Config)
}).toThrow(/unknown key "retryPolciy"/)
})
})
diff --git a/packages/llm/llm-retry/tests/transport-recovery.spec.ts b/packages/llm/llm-retry/tests/transport-recovery.spec.ts
index fb830f9868..7591f8fdf2 100644
--- a/packages/llm/llm-retry/tests/transport-recovery.spec.ts
+++ b/packages/llm/llm-retry/tests/transport-recovery.spec.ts
@@ -10,6 +10,7 @@ import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import type { MockLlmBehavior, MockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
import { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as Retry from '../src/index.ts'
let context: Context | undefined
@@ -37,6 +38,7 @@ async function harness(
vi.stubEnv('DEEPSEEK_API_KEY', 'mock-key')
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(LlmDeepSeek, {
baseURL,
streamIdleTimeoutMs: options.streamIdleTimeoutMs ?? 1_000,
diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json
index 1ec340226e..64c2eb0123 100644
--- a/packages/llm/llm/package.json
+++ b/packages/llm/llm/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-llm",
"description": "Provider-neutral LLM service interface for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/llm/plugin-package-inventory-deepseek/package.json b/packages/llm/plugin-package-inventory-deepseek/package.json
index 4b8eea1427..42fb4fd061 100644
--- a/packages/llm/plugin-package-inventory-deepseek/package.json
+++ b/packages/llm/plugin-package-inventory-deepseek/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-plugin-package-inventory-deepseek",
"description": "Active Loader-backed plugin package inventory for official DeepSeek LLM API requests",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
@@ -61,6 +61,7 @@
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
diff --git a/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts b/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts
index d6662caa2f..280afb90b8 100644
--- a/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts
+++ b/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts
@@ -8,6 +8,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { createScope } from '@deepseek-ai/dsh-scope'
import AgentPresets, { mountPreset } from '@deepseek-ai/dsh-agent-presets'
import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
@@ -44,6 +45,7 @@ async function harness(enabled?: boolean): Promise<{ ctx: Context; root: string;
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
await ctx.plugin(AgentRegistry)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentPresets, { default: 'fixture', roots: [], includeShippedRoot: false, includeUserRoot: false })
await ctx.plugin(DeepSeekLlmApiExtensionRegistry)
const inventory = enabled === undefined
diff --git a/packages/llm/token-meter/package.json b/packages/llm/token-meter/package.json
index 69ac327734..fcae3d1ccd 100644
--- a/packages/llm/token-meter/package.json
+++ b/packages/llm/token-meter/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-token-meter",
"description": "Replay-aware token measurement service (ctx.tokenMeter) for the DeepSeek Harness",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/llm/token-meter/src/index.ts b/packages/llm/token-meter/src/index.ts
index 082bfba087..e33c70b1df 100644
--- a/packages/llm/token-meter/src/index.ts
+++ b/packages/llm/token-meter/src/index.ts
@@ -10,7 +10,7 @@ import { BlockAssembler, deepFreeze } from '@deepseek-ai/dsh-llm'
import type { LlmImageRequestPricing, Message, TokenUsage } from '@deepseek-ai/dsh-llm'
import type { EpochHeader, Session, SessionEvent } from '@deepseek-ai/dsh-session'
import { canonicalHeader, headerEquals, isSurfaceEvent } from '@deepseek-ai/dsh-session'
-// Type-only: resolves the optional projection registry Context declaration.
+// Type-only: activates the `ctx.sessionProjections` Context declaration.
import type {} from '@deepseek-ai/dsh-session-projection'
import type {
TokenMeasurement,
@@ -25,6 +25,11 @@ import type { MeterSurfaceNode } from './surface-fold.ts'
import { priceSurface } from './route-pricing.ts'
export type * from './types.ts'
+// Module-edge re-export: forces the emitted index.d.ts to import the
+// projection-unit modules, so their SessionProjectionStateMap augmentations load
+// in aggregate programs that only import the package root.
+export type * from './usage-projection.ts'
+export type * from './breakdown-projection.ts'
/**
* Raw anchor facts captured at the latest successful call; the baseline is
@@ -85,19 +90,17 @@ export class TokenMeter extends Service {
// the public type excludes settings while validateConfigKeys rejects them.
static Config: z = z.object({}) as unknown as z
+ static inject = ['sessionProjections']
+
private readonly states = new WeakMap()
constructor(ctx: Context, config: TokenMeterConfig = {}) {
super(ctx, 'tokenMeter')
validateConfigKeys(config)
- // Projection registration is an optional child: compositions without the
- // generic registry keep the meter's standalone read shape.
- ctx.inject(['sessionProjections'], (projectionCtx) => {
- projectionCtx.sessionProjections.register(tokenUsageProjectionDefinition)
- projectionCtx.sessionProjections.register(contextPressureProjectionDefinition)
- projectionCtx.sessionProjections.register(contextBreakdownProjectionDefinition)
- })
+ ctx.sessionProjections.register(tokenUsageProjectionDefinition)
+ ctx.sessionProjections.register(contextPressureProjectionDefinition)
+ ctx.sessionProjections.register(contextBreakdownProjectionDefinition)
// Readers catch up independently, while eager observation bounds ordinary
// read latency without creating state for sessions no consumer has read.
diff --git a/packages/llm/token-meter/tests/token-usage-projection.spec.ts b/packages/llm/token-meter/tests/token-usage-projection.spec.ts
index 86f60392c9..f9ce9015e3 100644
--- a/packages/llm/token-meter/tests/token-usage-projection.spec.ts
+++ b/packages/llm/token-meter/tests/token-usage-projection.spec.ts
@@ -9,7 +9,6 @@ import TokenMeter from '@deepseek-ai/dsh-token-meter'
import type { ContextPressureProjection, TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client'
import { RetryId } from '@deepseek-ai/dsh-llm-retry'
import { CompactionId } from '@deepseek-ai/dsh-compaction'
-import type {} from '../src/usage-projection.ts'
const ZERO: TokenUsageProjection = {
uncachedInputTokens: 0,
diff --git a/packages/lsp/lsp-stdio/package.json b/packages/lsp/lsp-stdio/package.json
index 3dca6f9433..641f1b5002 100644
--- a/packages/lsp/lsp-stdio/package.json
+++ b/packages/lsp/lsp-stdio/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-lsp-stdio",
"description": "Generic stdio language-server provider for the DeepSeek Harness LSP capability seam (ctx.lsp) — spawns configured servers, translates JSON-RPC, and serves transient-open goToDefinition/findReferences/goToImplementation/hover queries in the host filesystem namespace",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/lsp/lsp/package.json b/packages/lsp/lsp/package.json
index 249b8249da..2b09bbe2be 100644
--- a/packages/lsp/lsp/package.json
+++ b/packages/lsp/lsp/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-lsp",
"description": "Abstract LSP capability seam (ctx.lsp) for the DeepSeek Harness — language-server provider registry keyed by branded id and extension mapping, order-independent per-query selection, normalized definition/references/implementation/hover requests and results, and the LspError taxonomy",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/lsp/tool-lsp/package.json b/packages/lsp/tool-lsp/package.json
index de95b13daa..5aab5810e9 100644
--- a/packages/lsp/tool-lsp/package.json
+++ b/packages/lsp/tool-lsp/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-lsp",
"description": "Model-facing lsp tool over the DeepSeek Harness LSP capability seam (ctx.lsp) — one read-only tool with goToDefinition/findReferences/goToImplementation/hover operations, one-based UTF-16 cursor coordinates, bounded location rendering, and hover normalization",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/mcp/mcp-client/README.i18n.yaml b/packages/mcp/mcp-client/README.i18n.yaml
index 1dd46a929f..5466123182 100644
--- a/packages/mcp/mcp-client/README.i18n.yaml
+++ b/packages/mcp/mcp-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/mcp/mcp-client/README.md
-README.md: 5349d0fece112e735b69c0137341bacd85888e81
-README.zh.md: bb01b326da70f1214572f56546e7f39658b9ff0c
+README.md: 929577e886b9f4a438739191191a1f0ee86c6343
+README.zh.md: ca07fd2b5d3547b09524a4097ce10bfb20251563
diff --git a/packages/mcp/mcp-client/README.md b/packages/mcp/mcp-client/README.md
index 5349d0fece..929577e886 100644
--- a/packages/mcp/mcp-client/README.md
+++ b/packages/mcp/mcp-client/README.md
@@ -126,7 +126,7 @@ The supervisor listens for `notifications/tools/list_changed` and queues a re-sy
### Tool execution internals
-A tool call sends an uncached `tools/call` request carrying the raw MCP name, the JSON arguments, the abort signal, and the configured timeout; the public name is never sent to the server and never parsed back. Canonical success is `{ content: JsonValue[], structuredContent? }`, preserving the complete MCP JSON blocks for programmatic and Code Mode callers. A supported advertised `outputSchema` validates `structuredContent`; unsupported schema vocabulary falls back to unconstrained `JsonValue`. An MCP `isError` result throws before any image persistence, so the registry produces a failed tool result. Image batches are decoded and validated as a whole before any member is saved; any refusal projects every image as diagnostic text.
+A tool call sends an uncached `tools/call` request carrying the raw MCP name, the JSON arguments, the abort signal, and the configured timeout; the public name is never sent to the server and never parsed back. Canonical success is `{ content: JsonValue[], structuredContent? }`, preserving the complete MCP JSON blocks for programmatic and PTC mode callers. A supported advertised `outputSchema` validates `structuredContent`; unsupported schema vocabulary falls back to unconstrained `JsonValue`. An MCP `isError` result throws before any image persistence, so the registry produces a failed tool result. Image batches are decoded and validated as a whole before any member is saved; any refusal projects every image as diagnostic text.
### Environment scrubbing (stdio)
@@ -171,7 +171,7 @@ The tool-definition prefix stays stable while the discovered set and schemas are
#### What the model sees
-The public tool name and JSON arguments remain in assistant history. The canonical value retains the complete MCP JSON blocks and optional structured content for programmatic and Code Mode callers; supported image blocks project beside text in their original order after exact route-capability proof. Refused images, audio, embedded resources, resource links, and unknown blocks remain visible as bounded text diagnostics, and MCP `isError` rejects the call before image persistence.
+The public tool name and JSON arguments remain in assistant history. The canonical value retains the complete MCP JSON blocks and optional structured content for programmatic and PTC mode callers; supported image blocks project beside text in their original order after exact route-capability proof. Refused images, audio, embedded resources, resource links, and unknown blocks remain visible as bounded text diagnostics, and MCP `isError` rejects the call before image persistence.
#### Token effect
diff --git a/packages/mcp/mcp-client/README.zh.md b/packages/mcp/mcp-client/README.zh.md
index bb01b326da..ca07fd2b5d 100644
--- a/packages/mcp/mcp-client/README.zh.md
+++ b/packages/mcp/mcp-client/README.zh.md
@@ -126,7 +126,7 @@ kind: "package-reference"
### 工具执行内部细节
-工具调用会发送一次未缓存的 `tools/call` 请求,携带原始 MCP 名称、JSON 参数、中止信号与配置的超时;公开名称绝不会发给服务器,也绝不会被解析还原。规范成功值是 `{ content: JsonValue[], structuredContent? }`,为程序化调用方与 Code Mode 调用方保留完整的 MCP JSON 块。受支持且已声明的 `outputSchema` 会验证 `structuredContent`;不受支持的 schema 词汇回退为不受约束的 `JsonValue`。MCP 的 `isError` 结果会在任何图片持久化之前抛出,使注册表产生失败的工具结果。图片批次会先整体解码并校验,再保存任一成员;任何拒绝都会把每张图片投影为诊断文本。
+工具调用会发送一次未缓存的 `tools/call` 请求,携带原始 MCP 名称、JSON 参数、中止信号与配置的超时;公开名称绝不会发给服务器,也绝不会被解析还原。规范成功值是 `{ content: JsonValue[], structuredContent? }`,为程序化调用方与 PTC mode 调用方保留完整的 MCP JSON 块。受支持且已声明的 `outputSchema` 会验证 `structuredContent`;不受支持的 schema 词汇回退为不受约束的 `JsonValue`。MCP 的 `isError` 结果会在任何图片持久化之前抛出,使注册表产生失败的工具结果。图片批次会先整体解码并校验,再保存任一成员;任何拒绝都会把每张图片投影为诊断文本。
### 环境清洗(stdio)
@@ -171,7 +171,7 @@ kind: "package-reference"
#### 模型看到什么
-公开工具名称和 JSON 参数保留在 assistant 历史中。规范值始终为程序化调用方与 Code Mode 调用方保留完整的 MCP JSON 块与可选结构化内容;受支持的图片块在确切路由能力得到证明后,按原始顺序与文本一起投影。被拒绝的图片、音频、嵌入资源、资源链接与未知块继续以有界文本诊断可见;MCP `isError` 会在图片持久化之前拒绝调用。
+公开工具名称和 JSON 参数保留在 assistant 历史中。规范值始终为程序化调用方与 PTC mode 调用方保留完整的 MCP JSON 块与可选结构化内容;受支持的图片块在确切路由能力得到证明后,按原始顺序与文本一起投影。被拒绝的图片、音频、嵌入资源、资源链接与未知块继续以有界文本诊断可见;MCP `isError` 会在图片持久化之前拒绝调用。
#### Token 影响
diff --git a/packages/mcp/mcp-client/package.json b/packages/mcp/mcp-client/package.json
index 37af73f583..fed592a473 100644
--- a/packages/mcp/mcp-client/package.json
+++ b/packages/mcp/mcp-client/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-mcp-client",
"description": "MCP client bridge: connects to MCP servers and registers their tools on ctx.tools",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/mcp/mcp-client/src/tools.ts b/packages/mcp/mcp-client/src/tools.ts
index 8c4780f71d..4c6ab40fbd 100644
--- a/packages/mcp/mcp-client/src/tools.ts
+++ b/packages/mcp/mcp-client/src/tools.ts
@@ -36,7 +36,7 @@ export interface ToolBridgeOptions {
/** State for one sync generation: the current set of disposers keyed by public name. */
export type ToolDisposers = Map void>
-/** Canonical MCP result exposed to Code Mode without discarding protocol blocks. */
+/** Canonical MCP result exposed to PTC mode without discarding protocol blocks. */
export type McpResult = {
content: JsonValue[]
structuredContent?: Structured
diff --git a/packages/plan/plan-mode/README.i18n.yaml b/packages/plan/plan-mode/README.i18n.yaml
index 31b11275ad..9dd9a5e938 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: 14c24507accb642909c1c9ed050368d9fa9120f1
-README.zh.md: 716f22a7b5bf7970b1eae429155d669805ce7d00
+README.md: e768a19c67b2973ff32aa0fe29e51d1b90ea96c9
+README.zh.md: 9d2b1ed6e1eebee6f3e9cd27fcc8ebad2c626a89
diff --git a/packages/plan/plan-mode/README.md b/packages/plan/plan-mode/README.md
index 14c24507ac..e768a19c67 100644
--- a/packages/plan/plan-mode/README.md
+++ b/packages/plan/plan-mode/README.md
@@ -82,7 +82,7 @@ Plan mode is a product package, not a capability seam: there is no swappable bac
### Durable state and step-boundary appends
-The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next accepted in-turn pre-step — the only append point while an agent runs — and an append failure cannot block the turn. The `set`/`get`/`foldPlanMode` helpers and their exact return states live in [`src/index.ts`](src/index.ts).
+The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next accepted in-turn pre-step — the only append point while an agent runs — and an append failure cannot block the turn. The `set`/`get` service methods and their exact return states live in [`src/index.ts`](src/index.ts) and read the registered `plan` projection; the first dependent access fails explicitly when the registry or key is absent.
### The `/plan` command
@@ -94,7 +94,7 @@ The command child activates only when a commands service is composed. It maps ba
### Session projection unit
-When `ctx.sessionProjections` is composed, the package registers the `plan` unit under an injected child. The unit turns logged `/plan` command runs into a candidate target, commits the logged state on `plan/mode`, and derives `{ active, pending }` for `view`, where `pending` is true only while an unsettled or successful selection differs from the logged state — a pure replay quantity recoverable from the log alone. The key merges into `SessionProjectionMap` from [`src/types.ts`](src/types.ts); the framework drives the unit, and unloading the plugin fiber unregisters the key.
+When `ctx.sessionProjections` is composed, the package registers the `plan` unit through optional injection. The unit turns logged `/plan` command runs into a candidate target, commits the logged state on `plan/mode`, and derives `{ active, pending }` for `view`, where `pending` is true only while an unsettled or successful selection differs from the logged state — a pure replay quantity recoverable from the log alone. The key merges into `SessionProjectionMap` from [`src/types.ts`](src/types.ts); the framework drives the unit, and unloading the plugin fiber unregisters the key. Plan-mode reads require this unit and the `turnBoundary` unit, and fail explicitly if the registry or either key is absent.
### Source map
diff --git a/packages/plan/plan-mode/README.zh.md b/packages/plan/plan-mode/README.zh.md
index 716f22a7b5..9d2b1ed6e1 100644
--- a/packages/plan/plan-mode/README.zh.md
+++ b/packages/plan/plan-mode/README.zh.md
@@ -82,7 +82,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并
### 持久状态与步骤边界追加
-本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有轮次开启时,模式变更会立即追加;轮次开启期间,它保持待生效,直到下一个被接受的轮内 pre-step——agent 运行时唯一的追加点——且追加失败不能阻塞轮次。`set`/`get`/`foldPlanMode` 辅助函数及其确切返回状态见 [`src/index.ts`](src/index.ts)。
+本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有轮次开启时,模式变更会立即追加;轮次开启期间,它保持待生效,直到下一个被接受的轮内 pre-step——agent 运行时唯一的追加点——且追加失败不能阻塞轮次。`set`/`get` 服务方法及其确切返回状态见 [`src/index.ts`](src/index.ts),并读取已注册的 `plan` 投影;注册表或 key 缺失时,第一次依赖它们的访问会显式失败。
### `/plan` 命令
@@ -94,7 +94,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并
### 会话投影单元
-组合 `ctx.sessionProjections` 时,本包在一个注入的子插件中注册 `plan` 单元。该单元把已记录的 `/plan` 命令运行转为候选目标,在 `plan/mode` 上提交已记录状态,并为 `view` 推导 `{ active, pending }`,其中 `pending` 仅在未结算或已成功的选择与已记录状态不同时为 true——这是仅凭日志即可恢复的纯回放量。key 由 [`src/types.ts`](src/types.ts) 的声明合并加入 `SessionProjectionMap`;框架负责驱动该单元,卸载插件 fiber 会注销该 key。
+组合了 `ctx.sessionProjections` 时,本包通过可选注入注册 `plan` 单元。该单元把已记录的 `/plan` 命令运行转为候选目标,在 `plan/mode` 上提交已记录状态,并为 `view` 推导 `{ active, pending }`,其中 `pending` 仅在未结算或已成功的选择与已记录状态不同时为 true——这是仅凭日志即可恢复的纯回放量。key 由 [`src/types.ts`](src/types.ts) 的声明合并加入 `SessionProjectionMap`;框架负责驱动该单元,卸载插件 fiber 会注销该 key。plan-mode 读取要求该单元与 `turnBoundary` 单元存在;注册表或任一 key 缺失时都会显式失败。
### 源码地图
diff --git a/packages/plan/plan-mode/package.json b/packages/plan/plan-mode/package.json
index 07e7d0cf4c..77162a5d81 100644
--- a/packages/plan/plan-mode/package.json
+++ b/packages/plan/plan-mode/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-plan-mode",
"description": "Logged per-agent plan mode with deployment guidance, a direct slash command, and a user-reviewed exit",
- "version": "0.1.1-rc.2",
+ "version": "0.1.2-alpha.1",
"publishConfig": {
"access": "public"
},
diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts
index 1664ccdaf4..7557174e13 100644
--- a/packages/plan/plan-mode/src/index.ts
+++ b/packages/plan/plan-mode/src/index.ts
@@ -6,12 +6,11 @@
* policy enforce restrictions independently and do not read or write plan
* state.
*
- * The state in force is folded from the session log (`plan/mode`, last one
- * wins), so resume and fork restore it without a live mirror. User selections
- * remain pending until the next accepted in-turn pre-step. The service includes
- * the selected state in the proposed step assembly, then appends `plan/mode`
- * from `agent/pre-step` only when the step is accepted. Same-step request
- * retries reuse their assembly.
+ * The `plan` projection folds the session log, so resume and fork restore the
+ * state. User selections remain pending until the next accepted in-turn
+ * pre-step. The service includes the selected state in the proposed step
+ * assembly, then appends `plan/mode` from `agent/pre-step` only when the step
+ * is accepted. Same-step request retries reuse their assembly.
*
* The exit tool remains registered while plan mode is inactive, so entering
* or leaving plan mode changes only the prompt section, not the request tool
@@ -28,19 +27,14 @@ import { z as zod } from 'zod'
import type { ZodType } from 'zod'
import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
-import type { Session, SessionEvent, UserMessage } from '@deepseek-ai/dsh-session'
+import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
import { UserQuestionError } from '@deepseek-ai/dsh-user-questions'
-// Type-only edge: resolves `ctx.commands` for the optional command child.
import type { CommandId } from '@deepseek-ai/dsh-commands'
-// Type-only: resolves ctx.sessionProjections for the optional unit child.
import type {} from '@deepseek-ai/dsh-session-projection'
-import type { PlanProjection } from './types.ts'
-// The `plan` projection-key declaration lives in src/types.ts (its one home);
-// this re-export projects the type face onto the package root AND keeps the
-// module edge in the emitted index.d.ts, so aggregate programs consuming the
-// declarations still receive the SessionProjectionMap merge.
+import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
+import type { PlanProjection, PlanUnitState } from './types.ts'
export type * from './types.ts'
declare module '@deepseek-ai/dsh-session/types' {
@@ -48,7 +42,7 @@ declare module '@deepseek-ai/dsh-session/types' {
/**
* Whether plan mode is in force from this point on: log-only, non-surface,
* whole-value replace. The last `plan/mode` wins; a log with none folds to
- * inactive through {@link foldPlanMode}.
+ * inactive through the projection unit's fold.
*/
'plan/mode': { active: boolean }
}
@@ -118,45 +112,6 @@ export function resolveConfig(config: PlanModeConfig): PlanModeConfig {
return { section }
}
-/**
- * Whether plan mode is active after the first `end` events. The last
- * `plan/mode` wins; a prefix with none is inactive.
- *
- * @param events The session log or any prefix of it.
- * @param end Fold `events[0, end)`; defaults to the whole log.
- * @returns Whether plan mode is active.
- */
-export function foldPlanMode(events: readonly SessionEvent[], end = events.length): boolean {
- let active = false
- let index = 0
- for (const event of events) {
- if (index >= end) break
- index++
- if (event.type === 'plan/mode') active = event.data.active
- }
- return active
-}
-
-/**
- * Projection unit state: the logged mode, the latest successful `/plan`
- * selection not yet resolved by a `plan/mode` commit, and an execution whose
- * paired `command/done` has not settled. Plain JSON (persisted-cache
- * precondition).
- */
-interface PlanUnitState {
- active: boolean
- /** The selection's target mode; null when no selection is outstanding. */
- wanted: boolean | null
- /** The latest plan command awaiting its paired settlement. */
- running: { commandId: CommandId; wanted: boolean } | null
-}
-
-declare module '@deepseek-ai/dsh-session-projection/types' {
- interface SessionProjectionStateMap {
- plan: PlanUnitState
- }
-}
-
const planUnitStateSchema: ZodType = zod.object({
active: zod.boolean(),
wanted: zod.boolean().nullable(),
@@ -164,6 +119,7 @@ const planUnitStateSchema: ZodType = zod.object({
commandId: zod.string() as unknown as ZodType,
wanted: zod.boolean(),
}).strict().nullable(),
+ activeAtLastHeader: zod.boolean().nullable(),
}).strict()
/** Wire payload schema of the `plan` projection. */
@@ -172,32 +128,45 @@ const planProjectionSchema: ZodType = zod.object({
pending: zod.boolean(),
})
-/** Whether the log holds an opened turn without its closing `turn/end`. */
-function hasOpenTurn(events: readonly SessionEvent[]): boolean {
- let open = false
- for (const event of events) {
- if (event.type === 'turn/start') open = true
- else if (event.type === 'turn/end') open = false
- }
- return open
-}
-
-/** Plan state at the last logged request header, or `undefined` before the first header. */
-function planModeAtLastHeader(events: readonly SessionEvent[]): boolean | undefined {
- let lastHeader = -1
- let index = 0
- for (const event of events) {
- if (event.type === 'request/header') lastHeader = index
- index++
- }
- if (lastHeader < 0) return undefined
- return foldPlanMode(events, lastHeader + 1)
-}
+/** Projection of logged plan selections and committed mode. */
+export const planProjectionDefinition = {
+ key: 'plan',
+ stateVersion: 3,
+ stateSchema: planUnitStateSchema,
+ init: () => ({ active: false, wanted: null, running: null, activeAtLastHeader: null }),
+ apply: (state, event) => {
+ if (event.type === 'command/run' && event.data.name === 'plan') {
+ if (event.data.args === undefined) return state
+ const wanted = event.data.args.trim() !== 'off'
+ return { ...state, running: { commandId: event.data.commandId, wanted } }
+ }
+ if (event.type === 'command/done' && event.data.commandId === state.running?.commandId) {
+ const wanted = event.data.kind === 'success' && state.running.wanted !== state.active
+ ? state.running.wanted
+ : null
+ return { ...state, wanted, running: null }
+ }
+ if (event.type === 'plan/mode') {
+ return { ...state, active: event.data.active, wanted: null }
+ }
+ if (event.type === 'request/header') {
+ return { ...state, activeAtLastHeader: state.active }
+ }
+ return state
+ },
+ wire: {
+ viewSchema: planProjectionSchema,
+ view: (state) => {
+ const wanted = state.running?.wanted ?? state.wanted
+ return { active: state.active, pending: wanted !== null && wanted !== state.active }
+ },
+ },
+} satisfies ProjectionDefinition<'plan', PlanUnitState>
/**
* `ctx.planMode`: owns logged plan state, applies and narrates selected state at step start,
* the `plan:policy` section, the `/plan` command, and the stable exit tool.
- * UIs observe committed flips through `session/event`; there is no live mirror.
+ * Client carriers expose the projection's cropped `{ active, pending }` view.
*/
export class PlanModeController extends Service {
static inject = ['tools', 'systemPrompt']
@@ -246,49 +215,13 @@ export class PlanModeController extends Service {
text: (context) => {
if (context.agent === undefined) return ''
const pending = this.pendingIntents.get(context.agent.session)
- return (pending?.active ?? foldPlanMode(context.agent.session.events)) ? this.section : ''
+ return (pending?.active ?? this.loggedActive(context.agent.session)) ? this.section : ''
},
})
- // The plan projection unit (session-projection RFC): a pure event fold
- // serving clients the whole {active, pending} value. `command/run`
- // records the user's logged /plan selection, its paired `command/done`
- // keeps only successful selections, and `plan/mode` records that
- // selection and clears it. Pending is thereby a pure
- // replay quantity: host restarts, other tabs, and cold reads all recover
- // it from the log alone. The unit child activates only when a projection
- // registry is composed (headless assemblies stay unaffected).
+ // The `plan` projection unit registers through the projection registry.
ctx.inject(['sessionProjections'], (projectionCtx) => {
- projectionCtx.sessionProjections.register<'plan', PlanUnitState>({
- key: 'plan',
- stateSchema: planUnitStateSchema,
- init: () => ({ active: false, wanted: null, running: null }),
- apply: (state, event) => {
- if (event.type === 'command/run' && event.data.name === 'plan') {
- if (event.data.args === undefined) return state
- const wanted = event.data.args.trim() !== 'off'
- return { ...state, running: { commandId: event.data.commandId, wanted } }
- }
- if (event.type === 'command/done' && event.data.commandId === state.running?.commandId) {
- const wanted = event.data.kind === 'success' && state.running.wanted !== state.active
- ? state.running.wanted
- : null
- return { ...state, wanted, running: null }
- }
- if (event.type === 'plan/mode') {
- return { ...state, active: event.data.active, wanted: null }
- }
- return state
- },
- wire: {
- viewSchema: planProjectionSchema,
- view: (state) => {
- const wanted = state.running?.wanted ?? state.wanted
- return { active: state.active, pending: wanted !== null && wanted !== state.active }
- },
- },
- stateVersion: 2,
- })
+ projectionCtx.sessionProjections.register(planProjectionDefinition)
})
// The command child activates only when a command registry is composed.
@@ -314,7 +247,7 @@ export class PlanModeController extends Service {
// Repeat the queued wording while an exit still awaits the
// next accepted pre-step; only a truly inactive session reads
// idempotent.
- return foldPlanMode(agent.session.events)
+ return this.loggedActive(agent.session)
? { kind: 'success', text: 'Leaving plan mode (applies from the next step).' }
: { kind: 'success', text: 'Plan mode is already inactive.' }
}
@@ -358,7 +291,7 @@ export class PlanModeController extends Service {
execute: async (args, exec) => {
const agent = exec.agent
if (agent === undefined) throw new Error(`${EXIT_PLAN_MODE} requires a calling agent (no session to switch)`)
- if (!foldPlanMode(agent.session.events)) {
+ if (!this.loggedActive(agent.session)) {
throw new Error(`${EXIT_PLAN_MODE} is only available in plan mode`)
}
if (!/^#\s+\S/.test(args.plan.trim())) {
@@ -430,6 +363,33 @@ export class PlanModeController extends Service {
}))
}
+ private loggedActive(session: Session): boolean {
+ return this.planState(session).active
+ }
+
+ private projectionRegistry(): Context['sessionProjections'] {
+ const projections = this.ctx.get('sessionProjections')
+ if (projections === undefined) throw new Error('plan-mode requires the session projection registry')
+ return projections
+ }
+
+ private hasOpenTurn(session: Session): boolean {
+ const state = this.projectionRegistry().stateOf(session, 'turnBoundary')
+ if (state === undefined) throw new Error('plan-mode requires the turnBoundary session projection')
+ return state.openTurnStartSeq !== null
+ }
+
+ private loggedActiveAtLastHeader(session: Session): boolean | undefined {
+ return this.planState(session).activeAtLastHeader ?? undefined
+ }
+
+ /** Read the required plan projection state or fail at the first service access. */
+ private planState(session: Session): PlanUnitState {
+ const state = this.projectionRegistry().stateOf(session, 'plan')
+ if (state === undefined) throw new Error('plan-mode requires the plan session projection')
+ return state
+ }
+
/**
* Read the logged plan state and any selected state awaiting the next
* accepted in-turn pre-step.
@@ -438,7 +398,7 @@ export class PlanModeController extends Service {
* @returns Current logged state plus a pending selection, when present.
*/
get(agent: Agent): { active: boolean; pending?: boolean } {
- const active = foldPlanMode(agent.session.events)
+ const active = this.loggedActive(agent.session)
const pending = this.pendingIntents.get(agent.session)
return pending === undefined ? { active } : { active, pending: pending.active }
}
@@ -462,15 +422,15 @@ export class PlanModeController extends Service {
set(agent: Agent, active: boolean): 'committed' | 'queued' | 'cancelled' | 'noop' {
const session = agent.session
const pending = this.pendingIntents.get(session)
- const target = pending?.active ?? foldPlanMode(session.events)
+ const target = pending?.active ?? this.loggedActive(session)
if (active === target) return 'noop'
- if (hasOpenTurn(session.events)) {
+ if (this.hasOpenTurn(session)) {
this.pendingIntents.set(session, { active, narrate: true })
- return foldPlanMode(session.events) === active ? 'cancelled' : 'queued'
+ return this.loggedActive(session) === active ? 'cancelled' : 'queued'
}
// No open turn: commit now. Delete only after append succeeds so a
// failed durable write leaves the selection retryable, not dropped.
- if (active === foldPlanMode(session.events)) {
+ if (active === this.loggedActive(session)) {
this.pendingIntents.delete(session)
return 'cancelled'
}
@@ -486,7 +446,7 @@ export class PlanModeController extends Service {
const pending = this.pendingIntents.get(session)
if (pending === undefined) return
const target = pending.active
- if (target === foldPlanMode(session.events)) {
+ if (target === this.loggedActive(session)) {
this.pendingIntents.delete(session)
return
}
@@ -498,7 +458,7 @@ export class PlanModeController extends Service {
/** Build a user-switch notice when the last logged header described the other mode. */
private narration(session: Session, target: boolean): UserMessage | undefined {
- const told = planModeAtLastHeader(session.events)
+ const told = this.loggedActiveAtLastHeader(session)
if (told === undefined || told === target) return
const text = target
? 'The user switched this session to plan mode.'
diff --git a/packages/plan/plan-mode/src/types.ts b/packages/plan/plan-mode/src/types.ts
index 8efd32f22a..3325c6b0c4 100644
--- a/packages/plan/plan-mode/src/types.ts
+++ b/packages/plan/plan-mode/src/types.ts
@@ -1,13 +1,15 @@
/**
* Pure types of the plan domain: the ONE home of the `plan` projection-key
- * declaration, free of this package's host-side value imports (cordis
- * service, dsh-tools, dsh-agent). Two namespace projections serve it —
- * `./types` for host consumers, `./client` for client aggregates — with zero
- * content duplication.
+ * declaration, free of this package's host-side value imports (cordis,
+ * dsh-tools, dsh-agent). Two namespace projections serve it — `./types` for
+ * host consumers and `./client` for client aggregates — with zero content
+ * duplication.
*
* @module @deepseek-ai/dsh-plan-mode/types
*/
+import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
+
/**
* The plan projection's wire value. `active` is the logged state in force
* (the last `plan/mode`, inactive before the first); `pending` is true while
@@ -21,7 +23,23 @@ export interface PlanProjection {
pending: boolean
}
+/** Host state used to derive {@link PlanProjection}. */
+export interface PlanUnitState {
+ /** Logged plan mode. */
+ active: boolean
+ /** The selection's target mode; null when no selection is outstanding. */
+ wanted: boolean | null
+ /** The latest plan command awaiting its paired settlement. */
+ running: { commandId: CommandId; wanted: boolean } | null
+ /** Active state recorded by the latest `request/header`, or null. */
+ activeAtLastHeader: boolean | null
+}
+
declare module '@deepseek-ai/dsh-session-projection/types' {
+ interface SessionProjectionStateMap {
+ /** Host plan-mode fold state. */
+ plan: PlanUnitState
+ }
interface SessionProjectionMap {
/** Plan collaboration state folded from the plan command lifecycle and `plan/mode` events. */
plan: PlanProjection
diff --git a/packages/plan/plan-mode/tests/integration.spec.ts b/packages/plan/plan-mode/tests/integration.spec.ts
index 77efc3beb3..a40df90638 100644
--- a/packages/plan/plan-mode/tests/integration.spec.ts
+++ b/packages/plan/plan-mode/tests/integration.spec.ts
@@ -6,7 +6,8 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
-import PlanModeController, { foldPlanMode } from '@deepseek-ai/dsh-plan-mode'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
+import PlanModeController from '@deepseek-ai/dsh-plan-mode'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
const PLAN_CONFIG = { section: 'Test plan mode instructions.' }
@@ -23,6 +24,7 @@ async function harness(adapter: MockAdapter): Promise {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
+ await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -51,6 +53,13 @@ function waitForIdle(ctx: Context, agent: Agent): Promise